diff --git a/.gitea/workflows/workflow.yaml b/.gitea/workflows/workflow.yaml index eaea6fc..041a02c 100644 --- a/.gitea/workflows/workflow.yaml +++ b/.gitea/workflows/workflow.yaml @@ -61,6 +61,48 @@ jobs: name: firefox-addon path: dist/ + build-for-safari: + name: 构建 Safari 扩展程序 + runs-on: macos + env: + VITE_RUN_ID: ${{ gitea.run_number }} + VITE_HASH_ID: ${{ gitea.sha }} + + steps: + - uses: actions/checkout@v3 + name: 检出代码 + + - name: 设置 Node.js + uses: actions/setup-node@v3 + with: + node-version: '22' + + - name: 安装依赖 + run: npm install + + - name: 构建扩展程序 + run: npm run build:safari + + - name: 设置 Xcode + uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: latest-stable + + - name: 创建 Safari 扩展项目 + run: | + xcrun safari-web-extension-converter dist --project-location safari-extension --app-name "MSR Mod" --bundle-identifier "moe.astrian.ext-msrmod" --swift --no-open + + - name: 构建 Safari 扩展 + run: | + cd "safari-extension/MSR Mod" + xcodebuild -project "MSR Mod.xcodeproj" -scheme "MSR Mod (macOS)" -configuration Release -destination "generic/platform=macOS" build + + - name: 上传构建工件 + uses: actions/upload-artifact@v3 + with: + name: safari-extension + path: safari-extension/ + publish-to-chrome-webstore: name: 发布至 Chrome 应用商店 runs-on: ubuntu-latest diff --git a/CLAUDE.md b/CLAUDE.md index d72ce3f..77f48a0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -19,6 +19,7 @@ npm i # Install dependencies ```bash npm run build:chrome # Build for Chrome/Chromium browsers npm run build:firefox # Build for Firefox +npm run build:safari # Build for Safari (uses background.html) npm run build # Default build (Chrome) ``` @@ -71,6 +72,7 @@ npm run qc # Alias for quality-check ### Browser Compatibility - **Chrome**: Uses service worker, full CSP support - **Firefox**: Uses background scripts, modified CSP, specific gecko settings +- **Safari**: Uses background page (background.html) instead of service worker - **Prebuild Scripts**: Automatically modify manifest.json and HTML for each platform ### Storage Strategy @@ -99,6 +101,7 @@ npm run qc # Alias for quality-check ### `/scripts/` - **prebuild-chrome.js**: Removes localhost dev configs for production - **prebuild-firefox.js**: Adapts manifest for Firefox compatibility +- **prebuild-safari.js**: Creates background.html and adapts manifest for Safari ### `/public/` - **manifest.json**: Extension manifest (modified by prebuild scripts) @@ -127,4 +130,30 @@ npm run qc # Alias for quality-check ### Error Handling - Graceful fallbacks for storage API unavailability - Resource URL rotation handling with automatic refresh -- Cross-browser compatibility with feature detection \ No newline at end of file +- Cross-browser compatibility with feature detection + +## Safari Extension Considerations + +### Background Script Handling +Safari Web Extensions have different requirements for background scripts: + +1. **Background Page vs Service Worker**: Safari uses `background.page` instead of `service_worker` +2. **Background HTML**: The prebuild script creates `background.html` that loads `background.js` +3. **Manifest Configuration**: Uses `"background": { "page": "background.html", "persistent": false }` + +### Auto-redirect Functionality +The auto-redirect feature in Safari may require special handling due to: +- Different WebKit extension APIs +- Safari's stricter security policies +- Tab management differences from Chromium + +### Building for Safari +```bash +npm run build:safari # Creates background.html and Safari-specific manifest +``` + +The Safari build process: +1. Removes localhost development configurations +2. Converts `service_worker` to `background.page` +3. Creates `background.html` wrapper for `background.js` +4. Adds Safari-specific browser settings \ No newline at end of file diff --git a/package.json b/package.json index e680d0f..c215a13 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "build": "echo 'No platform specified, will build for Chromium.' && npm run build-chrome", "build:chrome": "npm run prebuild:chrome && vue-tsc -b && vite build && cp -r public/* dist/", "build:firefox": "npm run prebuild:firefox && vue-tsc -b && vite build && cp -r public/* dist/", + "build:safari": "npm run prebuild:safari && vue-tsc -b && vite build && cp -r public/* dist/", "dev:refresh": "vue-tsc -b && vite build && cp -r public/* dist/", "build:watch": "vite build --watch", "preview": "vite preview", @@ -15,7 +16,8 @@ "quality-check": "biome ci", "qc": "npm run quality-check", "prebuild:chrome": "node scripts/prebuild-chrome.js", - "prebuild:firefox": "node scripts/prebuild-firefox.js" + "prebuild:firefox": "node scripts/prebuild-firefox.js", + "prebuild:safari": "node scripts/prebuild-safari.js" }, "dependencies": { "@tailwindcss/vite": "^4.1.7", diff --git a/public/manifest.json b/public/manifest.json index afdbec4..0211d94 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "MSR Mod", - "version": "0.0.6", + "version": "0.0.7", "description": "塞壬唱片(Monster Siren Records)官网的替代前端。", "content_scripts": [ { diff --git a/scripts/prebuild-safari.js b/scripts/prebuild-safari.js new file mode 100644 index 0000000..930e720 --- /dev/null +++ b/scripts/prebuild-safari.js @@ -0,0 +1,359 @@ +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +// 处理 manifest.json for Safari +function processManifest() { + const manifestPath = path.join(__dirname, '../public/manifest.json'); + const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8')); + + // 移除本地调试相关的配置 + if (manifest.host_permissions) { + manifest.host_permissions = manifest.host_permissions.filter( + permission => !permission.includes('localhost') + ); + } + + if (manifest.content_security_policy && manifest.content_security_policy.extension_pages) { + // 移除 CSP 中的本地开发相关配置 + manifest.content_security_policy.extension_pages = manifest.content_security_policy.extension_pages + .replace(/script-src 'self' http:\/\/localhost:5173;\s*/g, '') + .replace(/\s*http:\/\/localhost:5173\s*/g, ' ') + .replace(/\s*ws:\/\/localhost:5173\s*/g, ' ') + .replace(/;\s+/g, '; ') // 标准化分号后的空格 + .replace(/\s+/g, ' ') // 合并多个空格为一个 + .trim(); + } + + // Safari 特殊处理:添加 appShell.html 到 content scripts 匹配 + if (manifest.content_scripts && manifest.content_scripts[0]) { + // 添加 appShell.html 的匹配规则 + const existingMatches = manifest.content_scripts[0].matches; + if (!existingMatches.includes("https://monster-siren.hypergryph.com/")) { + existingMatches.push("https://monster-siren.hypergryph.com/"); + } + } + + // Safari 特殊处理:使用 background.page 而不是 service_worker + if (manifest.background && manifest.background.service_worker) { + // Safari 扩展在 Manifest V3 中必须使用 persistent: false + // 但为了调试,我们暂时设为 true 来确保页面加载 + manifest.background = { + page: "background.html", + persistent: true + }; + } + + // 创建 background.html 文件用于 Safari + const backgroundHtmlPath = path.join(__dirname, '../public/background.html'); + const backgroundHtmlContent = ` + +
+ +If you can see this page, the background page is loaded!
+ + + + + + +`; + fs.writeFileSync(backgroundHtmlPath, backgroundHtmlContent); + + // 创建 Safari 兼容的 background.js + const backgroundJsPath = path.join(__dirname, '../public/background.js'); + let backgroundJsContent = fs.readFileSync(backgroundJsPath, 'utf8'); + + // 检查是否已经添加过 Safari 代码,避免重复 + if (backgroundJsContent.includes('=== Safari background.js starting ===')) { + console.log('Safari background.js already processed, skipping...'); + } else { + // 在开头添加 Safari 调试信息(只添加一次) + const safariDebugCode = ` +console.log("=== Safari background.js starting ==="); +console.log("Available APIs:", { + chrome: typeof chrome, + browser: typeof browser, + safari: typeof safari +}); + +// Safari 特殊处理 +if (typeof chrome === 'undefined' && typeof browser === 'undefined') { + console.log("No extension APIs available in Safari"); + // 如果没有扩展 API,创建一个空的对象避免错误 + window.chrome = { + webRequest: { onBeforeRequest: { addListener: () => {} } }, + storage: { sync: { get: () => Promise.resolve({}) } }, + tabs: { create: () => {}, remove: () => {}, update: () => {} }, + runtime: { + getURL: (path) => path, + onMessage: { addListener: () => {} } + } + }; +} + +// Safari 消息监听器:处理来自 content script 的重定向请求 +if (typeof chrome !== 'undefined' && chrome.runtime && chrome.runtime.onMessage) { + chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { + console.log('Background received message:', message); + + if (message.action === 'redirect_to_extension') { + console.log('Processing redirect request from content script'); + + try { + // 创建新标签页并打开扩展 + const extensionUrl = chrome.runtime.getURL('index.html'); + chrome.tabs.create({ url: extensionUrl }, (newTab) => { + console.log('New extension tab created:', newTab.id); + + // 关闭原始标签页 + if (sender.tab && sender.tab.id) { + chrome.tabs.remove(sender.tab.id); + } + + sendResponse({ success: true, url: extensionUrl }); + }); + } catch (error) { + console.error('Failed to redirect:', error); + sendResponse({ success: false, error: error.message }); + } + + return true; // 保持消息通道开放 + } + }); +} + +`; + + // 替换 Safari 的重定向 URL 监听 + backgroundJsContent = backgroundJsContent.replace( + /{ urls: \['https:\/\/monster-siren\.hypergryph\.com\/api\/fontset', 'https:\/\/monster-siren\.hypergryph\.com\/manifest\.json'\] }/g, + "{ urls: ['https://monster-siren.hypergryph.com/api/fontset', 'https://monster-siren.hypergryph.com/manifest.json', 'https://monster-siren.hypergryph.com/'] }" + ); + + // 替换 Safari 的重定向判断逻辑 + backgroundJsContent = backgroundJsContent.replace( + /details\.url === 'https:\/\/monster-siren\.hypergryph\.com\/manifest\.json'/g, + "(details.url === 'https://monster-siren.hypergryph.com/manifest.json' || details.url === 'https://monster-siren.hypergryph.com/')" + ); + + // 清理可能的重复条件 + backgroundJsContent = backgroundJsContent.replace( + /\(\(details\.url === 'https:\/\/monster-siren\.hypergryph\.com\/manifest\.json' \|\| details\.url === 'https:\/\/monster-siren\.hypergryph\.com\/'\) \|\| details\.url === 'https:\/\/monster-siren\.hypergryph\.com\/'\)/g, + "(details.url === 'https://monster-siren.hypergryph.com/manifest.json' || details.url === 'https://monster-siren.hypergryph.com/')" + ); + + backgroundJsContent = safariDebugCode + backgroundJsContent; + } + fs.writeFileSync(backgroundJsPath, backgroundJsContent); + console.log('✅ Safari-compatible background.js created'); + + // 创建 Safari 专用的 content.js + const contentJsPath = path.join(__dirname, '../public/content.js'); + + // 检查是否已经处理过 content.js + const existingContentJs = fs.existsSync(contentJsPath) ? fs.readFileSync(contentJsPath, 'utf8') : ''; + if (existingContentJs.includes('checkRedirectPreference')) { + console.log('Safari content.js already processed, skipping...'); + } else { + const contentJsContent = ` +// Safari 扩展 content script for redirect +console.log('MSR Mod content script loaded on:', window.location.href); + +// 兼容 Safari 的浏览器 API +const browserAPI = typeof browser !== 'undefined' ? browser : chrome; + +// 异步函数:检查重定向偏好设置 +async function checkRedirectPreference() { + try { + console.log('Checking redirect preferences...'); + + // 读取偏好设置 + const pref = await browserAPI.storage.sync.get('preferences'); + console.log('Retrieved preferences:', pref); + + // 检查自动重定向设置(默认为 true) + const shouldRedirect = pref === undefined || + pref.preferences === undefined || + pref.preferences.autoRedirect === undefined || + pref.preferences.autoRedirect === true; + + console.log('Should redirect:', shouldRedirect); + return shouldRedirect; + } catch (error) { + console.error('Error reading preferences:', error); + // 如果读取偏好设置失败,默认重定向 + return true; + } +} + +// 执行重定向的函数 +function performRedirect() { + console.log('Performing redirect to extension...'); + + try { + // 对于 Safari,我们需要使用消息传递来请求重定向 + // 因为 content script 无法直接访问 chrome.runtime.getURL + + // 方案1:尝试通过消息传递 + if (typeof chrome !== 'undefined' && chrome.runtime) { + chrome.runtime.sendMessage({action: 'redirect_to_extension'}, (response) => { + if (chrome.runtime.lastError) { + console.log('Message sending failed, trying direct redirect...'); + // 方案2:尝试直接重定向(可能在某些情况下有效) + window.location.href = 'safari-web-extension://[extension-id]/index.html'; + } + }); + } else { + console.log('Chrome runtime not available, trying alternative redirect...'); + // 方案3:显示提示让用户手动打开扩展 + document.body.innerHTML = \` +Please click the MSR Mod extension icon in your Safari toolbar to open the app.
+ +最近两周有家事,同时我的 MacBook Pro 的 MagSafe 出了故障,还在 Genius Bar 维修,所以开发进程受到了一些影响。
+MSR Mod 现在有两种渠道接收错误及意见反馈。如果你对 MSR Mod 有任何的意见建议,或是想要回报错误及体验困惑之处,欢迎前往 GitHub Issue 或 Discord 社群 向我们反馈。如果你的意见或错误回报被接受,我们会将其放入 Trello 看板 中进行跟踪,敬请留意。
+ +