package browser import ( "context" "fmt" "net/http" "os" "path/filepath" "regexp" "strings" "time" ) const ( extensionDownloadTimeout = 90 * time.Second extensionMaxPackageBytes = 128 << 20 extensionsRootDir = "extensions" ) func ExtensionDownloadTimeout() time.Duration { return extensionDownloadTimeout } var extensionIDPattern = regexp.MustCompile(`^[a-p]{32}$`) type extensionManifest struct { Name string `json:"name"` Version string `json:"version"` Description string `json:"description"` ShortName string `json:"short_name"` DefaultLocale string `json:"default_locale"` Icons map[string]string `json:"icons"` Action map[string]any `json:"action"` BrowserAction map[string]any `json:"browser_action"` } func NormalizeExtensionID(value string) string { trimmed := strings.TrimSpace(value) if parsed := extractExtensionIDFromURL(trimmed); parsed != "" { trimmed = parsed } trimmed = strings.ToLower(strings.Trim(trimmed, "/#?& ")) if extensionIDPattern.MatchString(trimmed) { return trimmed } return "" } func BuildChromeWebStoreURL(extensionID string) string { normalizedID := NormalizeExtensionID(extensionID) if normalizedID == "" { return "" } return "https://chromewebstore.google.com/detail/" + normalizedID } func BuildChromeExtensionDownloadURL(extensionID string) string { normalizedID := NormalizeExtensionID(extensionID) if normalizedID == "" { return "" } return "https://clients2.google.com/service/update2/crx?response=redirect&prodversion=120.0.0.0&acceptformat=crx2,crx3&x=id%3D" + normalizedID + "%26installsource%3Dondemand%26uc" } func (m *Manager) LookupExtension(query string) (ExtensionLookupResult, error) { return m.LookupExtensionWithHTTPClient(query, nil) } func (m *Manager) LookupExtensionWithHTTPClient(query string, client *http.Client) (ExtensionLookupResult, error) { extensionID := NormalizeExtensionID(query) if extensionID == "" { return ExtensionLookupResult{}, fmt.Errorf("请输入 Chrome 插件 ID 或 Chrome Web Store 链接") } result := ExtensionLookupResult{ ExtensionID: extensionID, Name: extensionID, StoreURL: BuildChromeWebStoreURL(extensionID), Installable: true, Message: "已识别插件 ID,可下载安装", } data, err := downloadChromeExtensionCRX(context.Background(), extensionID, client) if err != nil { result.Message = "已识别插件 ID,但暂时无法读取商店元信息: " + err.Error() return result, nil } zipData, err := normalizeExtensionArchiveData(data) if err != nil { result.Message = "已识别插件 ID,但插件包格式无法解析: " + err.Error() return result, nil } manifestData, err := readExtensionManifestFromZip(zipData) if err != nil { result.Message = "已识别插件 ID,但 manifest 无法解析: " + err.Error() return result, nil } manifest, err := parseExtensionManifest(manifestData) if err != nil { result.Message = "已识别插件 ID,但 manifest 无法解析: " + err.Error() return result, nil } localeMessages := readExtensionLocaleMessagesFromZip(zipData, manifest) result.Name = resolveExtensionName(manifest, extensionID) result.Name = resolveExtensionMessage(result.Name, localeMessages) result.Version = strings.TrimSpace(manifest.Version) result.Description = resolveExtensionDescription(manifest, localeMessages) result.Message = "已读取插件信息,可下载安装" return ExtensionLookupResult{ ExtensionID: result.ExtensionID, Name: result.Name, Version: result.Version, Description: result.Description, StoreURL: result.StoreURL, Installable: result.Installable, Message: result.Message, }, nil } func (m *Manager) InstallExtensionFromWebStore(ctx context.Context, query string) (Extension, error) { return m.InstallExtensionFromWebStoreWithHTTPClient(ctx, query, nil) } func (m *Manager) InstallExtensionFromWebStoreWithHTTPClient(ctx context.Context, query string, client *http.Client) (Extension, error) { extensionID := NormalizeExtensionID(query) if extensionID == "" { return Extension{}, fmt.Errorf("请输入 Chrome 插件 ID 或 Chrome Web Store 链接") } data, err := downloadChromeExtensionCRX(ctx, extensionID, client) if err != nil { return Extension{}, err } return m.InstallExtensionPackageBytes(extensionID, BuildChromeWebStoreURL(extensionID), data) } func (m *Manager) InstallExtensionPackageBytes(extensionID string, sourceURL string, data []byte) (Extension, error) { if len(data) == 0 { return Extension{}, fmt.Errorf("插件包为空") } if len(data) > extensionMaxPackageBytes { return Extension{}, fmt.Errorf("插件包超过限制") } zipData, err := normalizeExtensionArchiveData(data) if err != nil { return Extension{}, err } manifestData, err := readExtensionManifestFromZip(zipData) if err != nil { return Extension{}, err } manifest, err := parseExtensionManifest(manifestData) if err != nil { return Extension{}, err } resolvedID := NormalizeExtensionID(extensionID) if resolvedID == "" { resolvedID = extensionIDFromManifest(manifestData) } if resolvedID == "" { return Extension{}, fmt.Errorf("无法识别插件 ID") } installDir := filepath.Join(m.ResolveRelativePath(filepath.Join("data", extensionsRootDir)), resolvedID) if err := replaceExtensionDirFromZip(zipData, installDir); err != nil { return Extension{}, err } localeMessages := readExtensionLocaleMessagesFromZip(zipData, manifest) manifestJSON := string(manifestData) extension := Extension{ ExtensionID: resolvedID, Name: resolveExtensionMessage(resolveExtensionName(manifest, resolvedID), localeMessages), Version: strings.TrimSpace(manifest.Version), Description: resolveExtensionDescription(manifest, localeMessages), IconDataURL: readExtensionIconDataURLFromZip(zipData, manifest), ManifestJSON: manifestJSON, SourceURL: strings.TrimSpace(sourceURL), InstallDir: installDir, Enabled: true, } if m.ExtensionDAO != nil { if err := m.ExtensionDAO.Upsert(extension); err != nil { return Extension{}, err } stored, err := m.ExtensionDAO.Get(resolvedID) if err == nil { return stored, nil } } return extension, nil } func (m *Manager) InstallExtensionPackageFile(path string) (Extension, error) { normalizedPath := strings.TrimSpace(path) if normalizedPath == "" { return Extension{}, fmt.Errorf("插件文件路径不能为空") } data, err := os.ReadFile(normalizedPath) if err != nil { return Extension{}, fmt.Errorf("读取插件文件失败: %w", err) } return m.InstallExtensionPackageBytes("", normalizedPath, data) } func (m *Manager) InstallExtensionDirectory(sourceDir string) (Extension, error) { normalizedDir := strings.TrimSpace(sourceDir) if normalizedDir == "" { return Extension{}, fmt.Errorf("插件目录不能为空") } manifestPath := filepath.Join(normalizedDir, "manifest.json") manifestData, err := os.ReadFile(manifestPath) if err != nil { return Extension{}, fmt.Errorf("插件目录缺少 manifest.json: %w", err) } manifest, err := parseExtensionManifest(manifestData) if err != nil { return Extension{}, err } extensionID := extensionIDFromManifest(manifestData) installDir := filepath.Join(m.ResolveRelativePath(filepath.Join("data", extensionsRootDir)), extensionID) if err := copyExtensionDirectory(normalizedDir, installDir); err != nil { return Extension{}, err } localeMessages := readExtensionLocaleMessagesFromDir(normalizedDir, manifest) extension := Extension{ ExtensionID: extensionID, Name: resolveExtensionMessage(resolveExtensionName(manifest, extensionID), localeMessages), Version: strings.TrimSpace(manifest.Version), Description: resolveExtensionDescription(manifest, localeMessages), IconDataURL: readExtensionIconDataURLFromDir(normalizedDir, manifest), ManifestJSON: string(manifestData), SourceURL: normalizedDir, InstallDir: installDir, Enabled: true, } if m.ExtensionDAO != nil { if err := m.ExtensionDAO.Upsert(extension); err != nil { return Extension{}, err } if stored, err := m.ExtensionDAO.Get(extensionID); err == nil { return stored, nil } } return extension, nil } func (m *Manager) EnabledExtensionDirs() []string { if m == nil || m.ExtensionDAO == nil { return nil } items, err := m.ExtensionDAO.ListEnabled() if err != nil { return nil } dirs := make([]string, 0, len(items)) for _, item := range items { dir := strings.TrimSpace(item.InstallDir) if dir == "" { continue } if _, err := os.Stat(filepath.Join(dir, "manifest.json")); err == nil { dirs = append(dirs, dir) } } return dirs } func (m *Manager) EnabledExtensionDirsForProfile(profileID string) []string { if m == nil || m.ExtensionDAO == nil { return nil } settings, err := m.ExtensionDAO.GetProfileSettings(profileID) if err != nil || !settings.Configured { return m.EnabledExtensionDirs() } items, err := m.ExtensionDAO.ListByIDs(settings.ExtensionIDs) if err != nil { return nil } dirs := make([]string, 0, len(items)) for _, item := range items { dir := strings.TrimSpace(item.InstallDir) if dir != "" { if _, err := os.Stat(filepath.Join(dir, "manifest.json")); err == nil { dirs = append(dirs, dir) } } } return dirs }