仰望星辰工作室

chore: 移除冒烟测试脚本与截图

F fqh 提交于 2026-10-02 23:07 · 477d947 ·父提交 b698572
chore: 移除冒烟测试脚本与截图
18 个文件变更 +3 -900 fqh@fangqihang.cn
•.gitignore +3 -0
•.smoke/01-splash.png 二进制
•.smoke/02-search.png 二进制
•.smoke/02b-theme-blue.png 二进制
•.smoke/03-playlists.png 二进制
•.smoke/04-settings.png 二进制
•.smoke/04b-settings-bottom.png 二进制
•.smoke/04c-theme-blue.png 二进制
•.smoke/05-account.png 二进制
•.smoke/06-about.png 二进制
•.smoke/07-search-back.png 二进制
•.smoke/icon-preview.png 二进制
•.smoke/pe-version.cjs +0 -288
•.smoke/release.txt +0 -4
•.smoke/report.json +0 -66
•.smoke/smoke.cjs +0 -284
•.smoke/verify-icon.cjs +0 -92
•.smoke/verify-nsis-text.cjs +0 -166
变更内容
diff --git a/.gitignore b/.gitignore
index 909ecc5..fc0fb1b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -28,3 +28,6 @@ client.log
 # OS
 .DS_Store
 Thumbs.db
+
+# Smoke 测试
+.smoke/
diff --git a/.smoke/01-splash.png b/.smoke/01-splash.png
deleted file mode 100644
index 8b76e42..0000000
Binary files a/.smoke/01-splash.png and /dev/null differ
diff --git a/.smoke/02-search.png b/.smoke/02-search.png
deleted file mode 100644
index 507913c..0000000
Binary files a/.smoke/02-search.png and /dev/null differ
diff --git a/.smoke/02b-theme-blue.png b/.smoke/02b-theme-blue.png
deleted file mode 100644
index 21b4b9f..0000000
Binary files a/.smoke/02b-theme-blue.png and /dev/null differ
diff --git a/.smoke/03-playlists.png b/.smoke/03-playlists.png
deleted file mode 100644
index a382481..0000000
Binary files a/.smoke/03-playlists.png and /dev/null differ
diff --git a/.smoke/04-settings.png b/.smoke/04-settings.png
deleted file mode 100644
index 7f6a247..0000000
Binary files a/.smoke/04-settings.png and /dev/null differ
diff --git a/.smoke/04b-settings-bottom.png b/.smoke/04b-settings-bottom.png
deleted file mode 100644
index dad6a8d..0000000
Binary files a/.smoke/04b-settings-bottom.png and /dev/null differ
diff --git a/.smoke/04c-theme-blue.png b/.smoke/04c-theme-blue.png
deleted file mode 100644
index 2d94709..0000000
Binary files a/.smoke/04c-theme-blue.png and /dev/null differ
diff --git a/.smoke/05-account.png b/.smoke/05-account.png
deleted file mode 100644
index aff37d8..0000000
Binary files a/.smoke/05-account.png and /dev/null differ
diff --git a/.smoke/06-about.png b/.smoke/06-about.png
deleted file mode 100644
index 0af58d5..0000000
Binary files a/.smoke/06-about.png and /dev/null differ
diff --git a/.smoke/07-search-back.png b/.smoke/07-search-back.png
deleted file mode 100644
index 507913c..0000000
Binary files a/.smoke/07-search-back.png and /dev/null differ
diff --git a/.smoke/icon-preview.png b/.smoke/icon-preview.png
deleted file mode 100644
index f36ebbe..0000000
Binary files a/.smoke/icon-preview.png and /dev/null differ
diff --git a/.smoke/pe-version.cjs b/.smoke/pe-version.cjs
deleted file mode 100644
index ea572e0..0000000
--- a/.smoke/pe-version.cjs
+++ /dev/null
@@ -1,288 +0,0 @@
-/**
- * PE 版本资源(RT_VERSION)解析器
- *
- * 为什么要自己写:本环境下 PowerShell 的 FileVersionInfo 能读,但 Shell.Application(COM)
- * 与 Add-Type 都被安全策略拦截。而 Windows 资源管理器「详细信息」标签页读取的正是
- * RT_VERSION 资源,所以这里直接解析 PE 与版本块结构给出结论。
- *
- * 输出:固定版本号、VarFileInfo\Translation、每个语言块的 StringFileInfo 键值。
- *
- * 用法:node .smoke/pe-version.cjs [exe 路径...]
- */
-
-const fs = require('fs')
-const path = require('path')
-
-const RT_VERSION = 16
-const align4 = (n) => (n + 3) & ~3
-
-const DEFAULT_TARGETS = [
-  path.resolve(__dirname, '..', 'build', 'bin', 'starmusic.exe'),
-  path.resolve(__dirname, '..', 'build', 'bin', 'starmusic-pc-1.3.0-Setup.exe'),
-]
-
-// ============================================================
-// PE 结构
-// ============================================================
-
-function readWStr(buf, offset, maxChars = 256) {
-  const chars = []
-  for (let i = 0; i < maxChars; i++) {
-    const c = buf.readUInt16LE(offset + i * 2)
-    if (c === 0) break
-    chars.push(c)
-  }
-  return String.fromCharCode(...chars)
-}
-
-function parsePE(buf) {
-  if (buf.readUInt16LE(0) !== 0x5a4d) throw new Error('缺少 MZ 头,不是 PE 文件')
-  const peOff = buf.readUInt32LE(0x3c)
-  if (buf.readUInt32LE(peOff) !== 0x00004550) throw new Error('缺少 PE 签名')
-  const coff = peOff + 4
-  const numSections = buf.readUInt16LE(coff + 2)
-  const optSize = buf.readUInt16LE(coff + 16)
-  const opt = coff + 20
-  const is64 = buf.readUInt16LE(opt) === 0x20b
-  const ddOff = opt + (is64 ? 112 : 96)
-  const resRVA = buf.readUInt32LE(ddOff + 2 * 8)
-  const resSize = buf.readUInt32LE(ddOff + 2 * 8 + 4)
-
-  const sections = []
-  const secOff = opt + optSize
-  for (let i = 0; i < numSections; i++) {
-    const o = secOff + i * 40
-    sections.push({
-      name: buf.toString('ascii', o, o + 8).replace(/\0+$/, ''),
-      virtualSize: buf.readUInt32LE(o + 8),
-      virtualAddress: buf.readUInt32LE(o + 12),
-      rawSize: buf.readUInt32LE(o + 16),
-      rawPtr: buf.readUInt32LE(o + 20),
-    })
-  }
-  return { sections, resRVA, resSize, is64 }
-}
-
-function rvaToOffset(sections, rva) {
-  for (const s of sections) {
-    const size = Math.max(s.virtualSize, s.rawSize)
-    if (rva >= s.virtualAddress && rva < s.virtualAddress + size) {
-      return s.rawPtr + (rva - s.virtualAddress)
-    }
-  }
-  return -1
-}
-
-/** 取指定资源类型的数据项列表 */
-function findResources(buf, pe, typeId) {
-  const resBase = rvaToOffset(pe.sections, pe.resRVA)
-  if (resBase < 0) return []
-  const dirAt = (rel) => ({
-    named: buf.readUInt16LE(resBase + rel + 12),
-    ided: buf.readUInt16LE(resBase + rel + 14),
-  })
-  const entryAt = (relDir, i) => {
-    const o = resBase + relDir + 16 + i * 8
-    return { name: buf.readUInt32LE(o), child: buf.readUInt32LE(o + 4) }
-  }
-
-  const out = []
-  const root = dirAt(0)
-  for (let t = 0; t < root.named + root.ided; t++) {
-    const te = entryAt(0, t)
-    if (te.name & 0x80000000) continue
-    if ((te.name & 0xffff) !== typeId) continue
-
-    const typeDirOff = te.child & 0x7fffffff
-    const typeDir = dirAt(typeDirOff)
-    for (let n = 0; n < typeDir.named + typeDir.ided; n++) {
-      const ne = entryAt(typeDirOff, n)
-      const langDirOff = ne.child & 0x7fffffff
-      const langDir = dirAt(langDirOff)
-      for (let l = 0; l < langDir.named + langDir.ided; l++) {
-        const le = entryAt(langDirOff, l)
-        const dataOff = resBase + (le.child & 0x7fffffff)
-        const rva = buf.readUInt32LE(dataOff)
-        const size = buf.readUInt32LE(dataOff + 4)
-        const offset = rvaToOffset(pe.sections, rva)
-        if (offset < 0) continue
-        out.push({ langId: le.name & 0x80000000 ? null : le.name & 0xffff, offset, size })
-      }
-    }
-  }
-  return out
-}
-
-// ============================================================
-// VERSIONINFO 块
-// ============================================================
-
-function parseNode(buf, start) {
-  const length = buf.readUInt16LE(start)
-  const valueLength = buf.readUInt16LE(start + 2)
-  const type = buf.readUInt16LE(start + 4)
-  const key = readWStr(buf, start + 6)
-  const keyBytes = (key.length + 1) * 2
-  return {
-    start,
-    length,
-    valueLength,
-    type,
-    key,
-    keyBytes,
-    valueOffset: start + align4(6 + keyBytes),
-    childrenOffset: start + align4(6 + keyBytes + valueLength),
-  }
-}
-
-/** 解析 StringFileInfo → [{ langKey, strings }] */
-function parseStringFileInfo(buf, blk) {
-  const node = parseNode(buf, blk)
-  const limit = blk + node.length
-  const blocks = []
-  let p = node.childrenOffset
-  while (p + 6 <= limit) {
-    const langNode = parseNode(buf, p)
-    if (!langNode.length) break
-    const strings = {}
-    const llimit = p + langNode.length
-    let q = langNode.childrenOffset
-    while (q + 6 <= llimit) {
-      const sNode = parseNode(buf, q)
-      if (!sNode.length) break
-      strings[sNode.key] = readWStr(buf, sNode.valueOffset, 4096)
-      q = align4(q + sNode.length)
-    }
-    blocks.push({ langKey: langNode.key, strings })
-    p = align4(p + langNode.length)
-  }
-  return blocks
-}
-
-/** 解析 VarFileInfo\Translation → [{lang, codePage}] */
-function parseTranslations(buf, vfiBlk) {
-  // VarFileInfo 自身没有值,真正的数据在其子节点 Translation 的 value 里
-  const trBlk = findChild(buf, vfiBlk, 'Translation')
-  if (trBlk < 0) return []
-  const node = parseNode(buf, trBlk)
-  const total = node.valueLength
-  const out = []
-  for (let i = 0; i + 4 <= total; i += 4) {
-    const d = buf.readUInt32LE(node.valueOffset + i)
-    out.push({ lang: d & 0xffff, codePage: (d >>> 16) & 0xffff })
-  }
-  return out
-}
-
-/** 在根节点的子节点里按 key 找 */
-function findChild(buf, rootBlk, key) {
-  const root = parseNode(buf, rootBlk)
-  const limit = rootBlk + root.length
-  let p = root.childrenOffset
-  while (p + 6 <= limit) {
-    const n = parseNode(buf, p)
-    if (!n.length) break
-    if (n.key === key) return p
-    p = align4(p + n.length)
-  }
-  return -1
-}
-
-// ============================================================
-// 主流程
-// ============================================================
-
-function analyze(file) {
-  console.log(`\n===== ${path.basename(file)} =====`)
-  const buf = fs.readFileSync(file)
-  let pe
-  try {
-    pe = parsePE(buf)
-  } catch (e) {
-    console.log(`  ✗ ${e.message}`)
-    return false
-  }
-
-  const vers = findResources(buf, pe, RT_VERSION)
-  if (!vers.length) {
-    console.log('  ✗ 未找到 RT_VERSION 资源')
-    return false
-  }
-  console.log(
-    `  RT_VERSION 资源:${vers.length} 个,资源条目语言 ID = ${vers
-      .map((v) => (v.langId == null ? '具名' : '0x' + v.langId.toString(16).padStart(4, '0')))
-      .join(', ')}`
-  )
-
-  const target = vers[0]
-  const root = parseNode(buf, target.offset)
-  console.log(`  根节点 key        : "${root.key}"${root.key === 'VS_VERSION_INFO' ? ' ✓' : ' ✗ 异常'}`)
-  if (root.key !== 'VS_VERSION_INFO') return false
-
-  const sig = buf.readUInt32LE(root.valueOffset)
-  const fileMS = buf.readUInt32LE(root.valueOffset + 8)
-  const fileLS = buf.readUInt32LE(root.valueOffset + 12)
-  const prodMS = buf.readUInt32LE(root.valueOffset + 16)
-  const prodLS = buf.readUInt32LE(root.valueOffset + 20)
-  const fmt = (ms, ls) => `${ms >>> 16}.${ms & 0xffff}.${ls >>> 16}.${ls & 0xffff}`
-  console.log(`  Signature         : 0x${sig.toString(16)}${sig === 0xfeef04bd ? ' ✓' : ' ✗'}`)
-  console.log(`  FileVersion       : ${fmt(fileMS, fileLS)}`)
-  console.log(`  ProductVersion    : ${fmt(prodMS, prodLS)}`)
-
-  const sfiBlk = findChild(buf, target.offset, 'StringFileInfo')
-  const vfiBlk = findChild(buf, target.offset, 'VarFileInfo')
-
-  if (vfiBlk >= 0) {
-    const tr = parseTranslations(buf, vfiBlk)
-    console.log(
-      `  Translation       : ${tr.map((t) => `lang=0x${t.lang.toString(16).padStart(4, '0')} cp=${t.codePage}`).join(', ')}`
-    )
-  } else {
-    console.log('  Translation       : ✗ 缺少 VarFileInfo(Win32 API 将无法定位字符串表)')
-  }
-
-  if (sfiBlk < 0) {
-    console.log('  StringFileInfo    : ✗ 缺失')
-    return false
-  }
-
-  const blocks = parseStringFileInfo(buf, sfiBlk)
-  if (!blocks.length) {
-    console.log('  StringFileInfo    : ✗ 无可解析的语言块')
-    return false
-  }
-
-  let nonEmpty = 0
-  let total = 0
-  for (const b of blocks) {
-    console.log(`\n  语言块 "${b.langKey}"  (资源管理器会按 Translation 拼出 "\\StringFileInfo\\${b.langKey}\\<键>" 来取值)`)
-    for (const [k, v] of Object.entries(b.strings)) {
-      total++
-      const ok = v.length > 0
-      if (ok) nonEmpty++
-      console.log(`    ${k.padEnd(18)} = ${ok ? JSON.stringify(v) : '(空)'}  ${ok ? '✓' : '✗'}`)
-    }
-  }
-
-  // Translation 与语言块 key 是否对得上(这是能否被 Win32 API 读到的关键)
-  if (vfiBlk >= 0) {
-    const tr = parseTranslations(buf, vfiBlk)
-    const expected = tr.map((t) => `${t.lang.toString(16).padStart(4, '0')}${t.codePage.toString(16).padStart(4, '0')}`)
-    const actual = blocks.map((b) => b.langKey.toLowerCase())
-    const matched = expected.every((e) => actual.includes(e))
-    console.log(
-      `\n  Translation 与语言块 key 对应关系:期望 [${expected.join(', ')}],实际 [${actual.join(', ')}] → ${
-        matched ? '✓ 一致' : '✗ 不一致(API 取不到字符串)'
-      }`
-    )
-    if (!matched) return false
-  }
-
-  console.log(`\n  结论:${nonEmpty}/${total} 个字符串字段有值`)
-  return nonEmpty > 0
-}
-
-const targets = process.argv.slice(2).length ? process.argv.slice(2) : DEFAULT_TARGETS
-const results = targets.map(analyze)
-console.log('')
-process.exit(results.every(Boolean) ? 0 : 1)
diff --git a/.smoke/release.txt b/.smoke/release.txt
deleted file mode 100644
index 0c6c25c..0000000
--- a/.smoke/release.txt
+++ /dev/null
@@ -1,4 +0,0 @@
-=== 分发产物 (pc/build/bin) ===
-  starmusic.exe                                  11.8 MB
-  starmusic-pc-1.3.0-Portable.zip                 4.7 MB
-  starmusic-pc-1.3.0-Setup.exe                    6.6 MB
diff --git a/.smoke/report.json b/.smoke/report.json
deleted file mode 100644
index 8c6254e..0000000
--- a/.smoke/report.json
+++ /dev/null
@@ -1,66 +0,0 @@
-{
-  "diag": {
-    "platform": "pc",
-    "native": false,
-    "viewSearch": 1,
-    "navItems": 5,
-    "playerBar": true,
-    "rootStyle": "--theme-color: #31c27c; --theme-color-rgb: 49, 194, 124; --theme-color-dark: #1ba56a; --theme-color-soft: #e8f8f0;",
-    "audioMethods": {
-      "onPlay": "function",
-      "onPause": "function",
-      "onStop": "function",
-      "onEnded": "function",
-      "onCanplay": "function",
-      "onError": "function",
-      "onTimeUpdate": "function",
-      "offTimeUpdate": "function",
-      "onSeeked": "function"
-    },
-    "bootBanner": null,
-    "accountCard": "未登录点击登录账号",
-    "themeBefore": "#31c27c",
-    "themeAfter": "#3b82f6",
-    "storedSetting": "{\"common.apiSource\":\"\",\"common.theme\":\"blue\",\"common.bgImageEnabled\":false,\"common.bgImageBlur\":12,\"player.playQuality\":\"128k\",\"player.downloadQuality\":\"\",\"player.togglePlayMethod\":\"listLoop\",\"search.isShowHotSearch\":false,\"search.isShowHistorySearch\":false,\"player.volume\":1,\"player.playbackRate\":1}"
-  },
-  "exceptions": [],
-  "consoleSample": [
-    {
-      "type": "log:error",
-      "text": "Failed to load resource: the server responded with a status of 404 (Not Found)"
-    },
-    {
-      "type": "log:error",
-      "text": "Access to fetch at 'http://hotword.kuwo.cn/hotword.s?prod=kwplayer_ar_9.3.0.1&corp=kuwo&newver=2&vipver=9.3.0.1&source=kwplayer_ar_9.3.0.1_40.apk&p2p=1&notrace=0&uid=0&plat=kwplayer_ar&rformat=json&encoding=utf8&tabid=1' from origin 'http://127.0.0.1:8899' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource."
-    },
-    {
-      "type": "log:error",
-      "text": "Failed to load resource: net::ERR_FAILED"
-    },
-    {
-      "type": "log",
-      "text": "[httpFetch error] http://hotword.kuwo.cn/hotword.s?prod=kwplayer_ar_9.3.0.1&corp=kuwo&newver=2&vipver=9.3.0.1&source=kwplayer_ar_9.3.0.1_40.apk&p2p=1&notrace=0&uid=0&plat=kwplayer_ar&rformat=json&encoding=utf8&tabid=1 Error: request:fail Failed to fetch\n    at fail (http://127.0.0.1:8899/assets/index-9I8XxacX.js:32:32178)\n    at http://127.0.0.1:8899/assets/index-9I8XxacX.js:31:5793"
-    },
-    {
-      "type": "log",
-      "text": "[easter-egg] 拉取成功:3 条彩蛋配置"
-    },
-    {
-      "type": "log:verbose",
-      "text": "[DOM] Password field is not contained in a form: (More info: https://www.chromium.org/developers/design-documents/create-amazing-password-forms) %o"
-    },
-    {
-      "type": "log:error",
-      "text": "Access to fetch at 'http://hotword.kuwo.cn/hotword.s?prod=kwplayer_ar_9.3.0.1&corp=kuwo&newver=2&vipver=9.3.0.1&source=kwplayer_ar_9.3.0.1_40.apk&p2p=1&notrace=0&uid=0&plat=kwplayer_ar&rformat=json&encoding=utf8&tabid=1' from origin 'http://127.0.0.1:8899' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource."
-    },
-    {
-      "type": "log:error",
-      "text": "Failed to load resource: net::ERR_FAILED"
-    },
-    {
-      "type": "log",
-      "text": "[httpFetch error] http://hotword.kuwo.cn/hotword.s?prod=kwplayer_ar_9.3.0.1&corp=kuwo&newver=2&vipver=9.3.0.1&source=kwplayer_ar_9.3.0.1_40.apk&p2p=1&notrace=0&uid=0&plat=kwplayer_ar&rformat=json&encoding=utf8&tabid=1 Error: request:fail Failed to fetch\n    at fail (http://127.0.0.1:8899/assets/index-9I8XxacX.js:32:32178)\n    at http://127.0.0.1:8899/assets/index-9I8XxacX.js:31:5793"
-    }
-  ],
-  "consoleErrorCount": 5
-}
\ No newline at end of file
diff --git a/.smoke/smoke.cjs b/.smoke/smoke.cjs
deleted file mode 100644
index db604aa..0000000
--- a/.smoke/smoke.cjs
+++ /dev/null
@@ -1,284 +0,0 @@
-/**
- * 冒烟测试:用无头 Edge(Chromium) 加载 PC 版前端产物,
- * 通过 CDP 收集控制台输出 / 未捕获异常,并逐屏截图。
- *
- * 说明:浏览器环境没有 Wails 注入的 window.go,hasNative() 返回 false,
- * 网络请求会走 fetch 并因 CORS 失败——这些属于预期内的情况,会单独标注。
- */
-const http = require('http')
-const fs = require('fs')
-const path = require('path')
-const { spawn } = require('child_process')
-
-const DIST = 'C:\\Users\\admin\\Documents\\HBuilderProjects\\starmusic\\pc\\frontend\\dist'
-const OUT = 'C:\\Users\\admin\\Documents\\HBuilderProjects\\starmusic\\pc\\.smoke'
-const PORT = 8899
-const CDP_PORT = 9222
-const EDGE = 'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe'
-const USER_DATA = path.join(OUT, 'edge-profile')
-
-const MIME = {
-  '.html': 'text/html; charset=utf-8',
-  '.js': 'text/javascript; charset=utf-8',
-  '.css': 'text/css; charset=utf-8',
-  '.svg': 'image/svg+xml',
-  '.png': 'image/png',
-  '.json': 'application/json; charset=utf-8',
-  '.woff2': 'font/woff2',
-}
-
-const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
-
-function startServer() {
-  const server = http.createServer((req, res) => {
-    let rel = decodeURIComponent(req.url.split('?')[0])
-    if (rel === '/') rel = '/index.html'
-    const file = path.join(DIST, rel)
-    if (!file.startsWith(DIST) || !fs.existsSync(file) || fs.statSync(file).isDirectory()) {
-      res.writeHead(404)
-      res.end('not found')
-      return
-    }
-    res.writeHead(200, { 'Content-Type': MIME[path.extname(file)] || 'application/octet-stream' })
-    fs.createReadStream(file).pipe(res)
-  })
-  return new Promise((resolve) => server.listen(PORT, '127.0.0.1', () => resolve(server)))
-}
-
-function getJSON(url) {
-  return new Promise((resolve, reject) => {
-    http
-      .get(url, (res) => {
-        let body = ''
-        res.on('data', (c) => (body += c))
-        res.on('end', () => {
-          try {
-            resolve(JSON.parse(body))
-          } catch (e) {
-            reject(e)
-          }
-        })
-      })
-      .on('error', reject)
-  })
-}
-
-async function waitForTarget(timeoutMs = 20000) {
-  const started = Date.now()
-  while (Date.now() - started < timeoutMs) {
-    try {
-      const list = await getJSON(`http://127.0.0.1:${CDP_PORT}/json/list`)
-      const page = list.find((t) => t.type === 'page' && t.webSocketDebuggerUrl)
-      if (page) return page
-    } catch (e) {
-      /* 还没起来 */
-    }
-    await sleep(400)
-  }
-  throw new Error('CDP target not found')
-}
-
-async function main() {
-  fs.mkdirSync(OUT, { recursive: true })
-  if (fs.existsSync(USER_DATA)) fs.rmSync(USER_DATA, { recursive: true, force: true })
-
-  const server = await startServer()
-  console.log('[smoke] static server up on', PORT)
-
-  const child = spawn(
-    EDGE,
-    [
-      '--headless=new',
-      '--disable-gpu',
-      '--hide-scrollbars',
-      '--no-first-run',
-      '--no-default-browser-check',
-      `--remote-debugging-port=${CDP_PORT}`,
-      `--user-data-dir=${USER_DATA}`,
-      '--window-size=1400,900',
-      `http://127.0.0.1:${PORT}/`,
-    ],
-    { stdio: 'ignore', detached: false },
-  )
-
-  const target = await waitForTarget()
-  console.log('[smoke] page target:', target.url)
-
-  const ws = new WebSocket(target.webSocketDebuggerUrl)
-  let msgId = 0
-  const pending = new Map()
-  const consoleLines = []
-  const exceptions = []
-
-  const send = (method, params = {}) =>
-    new Promise((resolve, reject) => {
-      const id = ++msgId
-      pending.set(id, { resolve, reject })
-      ws.send(JSON.stringify({ id, method, params }))
-    })
-
-  await new Promise((resolve, reject) => {
-    ws.addEventListener('open', resolve)
-    ws.addEventListener('error', reject)
-  })
-
-  ws.addEventListener('message', (ev) => {
-    let msg
-    try {
-      msg = JSON.parse(ev.data)
-    } catch (e) {
-      return
-    }
-    if (msg.id && pending.has(msg.id)) {
-      const { resolve, reject } = pending.get(msg.id)
-      pending.delete(msg.id)
-      if (msg.error) reject(new Error(JSON.stringify(msg.error)))
-      else resolve(msg.result)
-      return
-    }
-    if (msg.method === 'Runtime.consoleAPICalled') {
-      const text = (msg.params.args || [])
-        .map((a) => (a.value !== undefined ? a.value : a.description || a.type))
-        .join(' ')
-      consoleLines.push({ type: msg.params.type, text })
-    }
-    if (msg.method === 'Runtime.exceptionThrown') {
-      const d = msg.params.exceptionDetails || {}
-      exceptions.push({
-        text: d.text || '',
-        desc: (d.exception && (d.exception.description || d.exception.value)) || '',
-        line: d.lineNumber,
-        url: d.url,
-      })
-    }
-    if (msg.method === 'Log.entryAdded') {
-      const e = msg.params.entry || {}
-      consoleLines.push({ type: `log:${e.level}`, text: e.text || '' })
-    }
-  })
-
-  await send('Runtime.enable')
-  await send('Log.enable')
-  await send('Page.enable')
-
-  const shot = async (name) => {
-    try {
-      const res = await send('Page.captureScreenshot', { format: 'png' })
-      fs.writeFileSync(path.join(OUT, `${name}.png`), Buffer.from(res.data, 'base64'))
-      console.log('[smoke] screenshot:', name)
-    } catch (e) {
-      console.log('[smoke] screenshot failed', name, e.message)
-    }
-  }
-
-  const evaluate = async (expr) => {
-    try {
-      const res = await send('Runtime.evaluate', { expression: expr, returnByValue: true, awaitPromise: true })
-      return res.result ? res.result.value : undefined
-    } catch (e) {
-      return `<<eval error: ${e.message}>>`
-    }
-  }
-
-  // 启动页
-  await sleep(1600)
-  await shot('01-splash')
-
-  // 主界面(启动页 2.6s 后自动进入)
-  await sleep(3200)
-  await shot('02-search')
-
-  const diag = {}
-  diag.platform = await evaluate('window.uni && window.uni.__platform')
-  diag.native = await evaluate('window.uni && window.uni.__native')
-  diag.viewSearch = await evaluate("document.querySelectorAll('.view-search').length")
-  diag.navItems = await evaluate("document.querySelectorAll('.nav-item').length")
-  diag.playerBar = await evaluate("!!document.querySelector('.player-bar')")
-  diag.rootStyle = await evaluate(
-    "(()=>{const el=document.querySelector('.app-root');return el?el.getAttribute('style'):null})()",
-  )
-
-  // 音频上下文:验证 onTimeUpdate 等事件注册方法存在(此前这里是致命 bug)
-  diag.audioMethods = await evaluate(
-    "(()=>{const a=window.uni.createInnerAudioContext();const keys=['onPlay','onPause','onStop','onEnded','onCanplay','onError','onTimeUpdate','offTimeUpdate','onSeeked'];const r={};keys.forEach(k=>{r[k]=typeof a[k]});return r})()",
-  )
-  diag.bootBanner = await evaluate(
-    "(()=>{const el=document.querySelector('.boot-error');return el?el.textContent.trim():null})()",
-  )
-
-  // 逐屏切换
-  const clickNav = async (index, name) => {
-    await evaluate(`(()=>{const a=document.querySelectorAll('.nav-item');if(a[${index}])a[${index}].click()})()`)
-    await sleep(900)
-    await shot(name)
-  }
-
-  await clickNav(1, '03-playlists')
-  await clickNav(2, '04-settings')
-
-  // 账号入口:验证侧栏底部账号卡片渲染(未登录态显示「未登录 / 点击登录账号」)
-  diag.accountCard = await evaluate(
-    "(()=>{const el=document.querySelector('.account-card');return el?el.textContent.replace(/\\s+/g,' ').trim():null})()",
-  )
-
-  // 主题切换:验证 CSS 变量是否随设置变化(确认 .app-root 的内联变量生效)
-  // 主题快切入口已在设置页(侧栏原来那排 .swatch 色块改成了账号入口),
-  // 所以这里点设置页的 .theme-item
-  diag.themeBefore = await evaluate(
-    "getComputedStyle(document.querySelector('.app-root')).getPropertyValue('--theme-color').trim()",
-  )
-  await evaluate("(()=>{const t=document.querySelectorAll('.theme-item');if(t[1])t[1].click()})()")
-  await sleep(600)
-  diag.themeAfter = await evaluate(
-    "getComputedStyle(document.querySelector('.app-root')).getPropertyValue('--theme-color').trim()",
-  )
-  diag.storedSetting = await evaluate(
-    "(()=>{try{return localStorage.getItem('starmusic_setting')}catch(e){return 'ERR'}})()",
-  )
-  await shot('04c-theme-blue')
-  // 切回默认主题,避免影响后续截图
-  await evaluate("(()=>{const t=document.querySelectorAll('.theme-item');if(t[0])t[0].click()})()")
-  await sleep(400)
-
-  // 设置页较长,滚到底部验证「诊断 / 关于」区域(数据目录、日志、更新等)
-  await evaluate(
-    "(()=>{const el=document.querySelector('.view-settings');if(el)el.scrollTop=el.scrollHeight})()",
-  )
-  await sleep(600)
-  await shot('04b-settings-bottom')
-
-  await clickNav(3, '05-account')
-  await clickNav(4, '06-about')
-
-  // 回到搜索页,验证搜索历史 / 热搜区域渲染
-  await clickNav(0, '07-search-back')
-
-  const result = {
-    diag,
-    exceptions,
-    consoleSample: consoleLines.slice(0, 60),
-    consoleErrorCount: consoleLines.filter((l) => l.type === 'error' || l.type === 'log:error').length,
-  }
-  fs.writeFileSync(path.join(OUT, 'report.json'), JSON.stringify(result, null, 2))
-
-  console.log('\n===== DIAG =====')
-  console.log(JSON.stringify(diag, null, 2))
-  console.log('\n===== EXCEPTIONS (' + exceptions.length + ') =====')
-  exceptions.slice(0, 20).forEach((e) => console.log(' -', e.text, '|', String(e.desc).split('\n')[0]))
-  console.log('\n===== CONSOLE ERRORS/WARNS =====')
-  consoleLines
-    .filter((l) => /error|warn/.test(l.type))
-    .slice(0, 30)
-    .forEach((l) => console.log(` [${l.type}] ${l.text.slice(0, 220)}`))
-
-  ws.close()
-  child.kill()
-  server.close()
-  await sleep(300)
-  process.exit(0)
-}
-
-main().catch((e) => {
-  console.error('[smoke] FAILED', e)
-  process.exit(1)
-})
diff --git a/.smoke/verify-icon.cjs b/.smoke/verify-icon.cjs
deleted file mode 100644
index bc224c3..0000000
--- a/.smoke/verify-icon.cjs
+++ /dev/null
@@ -1,92 +0,0 @@
-/**
- * 验证 exe 内是否嵌入了指定图标
- *
- * 原理:我们的 icon.ico 里每个尺寸都是 PNG 条目。Wails 会把 icon.ico 作为
- * Windows 资源(RT_GROUP_ICON / RT_ICON)写入 exe。若 exe 中能找到 icon.ico
- * 里某个尺寸的完整 PNG 字节序列,就说明新图标确实被嵌入(且不是模板默认图标)。
- *
- * 用法:node .smoke/verify-icon.cjs
- */
-
-const fs = require('fs')
-const path = require('path')
-
-const ROOT = path.resolve(__dirname, '..')
-const ICO = path.join(ROOT, 'build', 'windows', 'icon.ico')
-const DEFAULT_ICO_MARKER_SIZE = 256
-
-const targets = [
-  path.join(ROOT, 'build', 'bin', 'starmusic.exe'),
-  path.join(ROOT, 'build', 'bin', 'starmusic-pc-1.3.0-Setup.exe'),
-]
-
-/** 解析 ICO,返回 [{ size, png }] */
-function parseICO(buf) {
-  const count = buf.readUInt16LE(4)
-  const entries = []
-  for (let i = 0; i < count; i++) {
-    const o = 6 + i * 16
-    const size = buf[o] === 0 ? 256 : buf[o]
-    const len = buf.readUInt32LE(o + 8)
-    const off = buf.readUInt32LE(o + 12)
-    const data = buf.subarray(off, off + len)
-    const isPng = data[0] === 0x89 && data[1] === 0x50
-    entries.push({ size, len, isPng, png: isPng ? data : null })
-  }
-  return entries
-}
-
-const ico = fs.readFileSync(ICO)
-const entries = parseICO(ico)
-
-console.log(`icon.ico:${ico.length} 字节,${entries.length} 档尺寸`)
-console.log(
-  '  尺寸分布:' + entries.map((e) => `${e.size}${e.isPng ? 'P' : 'B'}`).join('  ') + '   (P=PNG 条目, B=BMP 条目)'
-)
-console.log('')
-
-let failed = 0
-
-for (const exe of targets) {
-  if (!fs.existsSync(exe)) {
-    console.log(`✗ 找不到 ${path.basename(exe)}`)
-    failed++
-    continue
-  }
-  const bin = fs.readFileSync(exe)
-  const hits = []
-  for (const e of entries) {
-    if (!e.png) continue
-    const found = bin.includes(e.png)
-    if (found) hits.push(e.size)
-  }
-  const name = path.basename(exe)
-  if (hits.length) {
-    console.log(`✓ ${name}(${(bin.length / 1024 / 1024).toFixed(1)} MB)`)
-    console.log(`    命中内嵌图标尺寸:${hits.join(', ')}  → 新图标已嵌入`)
-  } else {
-    console.log(`✗ ${name}:未找到任何一档图标字节,图标可能未被嵌入`)
-    failed++
-  }
-}
-
-console.log('')
-// 对照:确认模板默认图标已不在(避免"改了但没生效")
-const templateIco = 'C:\\Users\\admin\\AppData\\Local\\Temp\\wailstpl\\probe\\build\\windows\\icon.ico'
-if (fs.existsSync(templateIco)) {
-  const t = parseICO(fs.readFileSync(templateIco))
-  const tEntry = t.find((e) => e.png)
-  const exe = fs.readFileSync(targets[0])
-  if (tEntry && exe.includes(tEntry.png)) {
-    console.log('! 警告:exe 中仍能找到 Wails 模板默认图标的字节,请确认图标替换是否生效')
-    failed++
-  } else {
-    console.log('✓ 对照检查:exe 中不含 Wails 模板默认图标(已彻底替换)')
-  }
-}
-
-if (failed) {
-  console.error(`\n验证失败:${failed} 项`)
-  process.exit(1)
-}
-console.log('\n图标嵌入验证通过')
diff --git a/.smoke/verify-nsis-text.cjs b/.smoke/verify-nsis-text.cjs
deleted file mode 100644
index 8cba458..0000000
--- a/.smoke/verify-nsis-text.cjs
+++ /dev/null
@@ -1,166 +0,0 @@
-/**
- * 验证安装包中文编码不会乱码
- *
- * 为什么需要单独验证:NSIS 在 `Unicode true` 下若读不到 UTF-8 BOM,会按系统 ANSI 代码页
- * (中文系统为 GBK)解码 .nsi —— 编译不会报错,但用户看到的向导是全屏乱码。这个坑不会
- * 在任何构建日志里暴露,必须主动检查。
- *
- * 验证链(三条互相独立,任一不成立都会报错):
- *   A. project.nsi 必须带 UTF-8 BOM,且内容为合法 UTF-8
- *   B. makensis 编译时须自己报告 `Processing script file: "project.nsi" (UTF8)`
- *      —— 这是 makensis 的编码识别结果,等价于"它按 UTF-8 读了"
- *   C. 最终安装包(未压缩的 PE 版本资源区)中能找到**仅来自 project.nsi** 的中文文案
- *      的正确 UTF-16LE 编码 —— 证明中文从源文件一路正确走到了成品
- *
- * 说明:向导正文(MUI 文案 / MessageBox / DetailPrint)会被 NSIS 压缩存放,
- * 无法在成品中直接搜索;它们与 C 中的文案同处一个文件、同一条编码链路,
- * 因此 A + B + C 成立即可判定全部中文正确。
- *
- * 用法:node .smoke/verify-nsis-text.cjs
- */
-
-const fs = require('fs')
-const path = require('path')
-const { execFileSync } = require('child_process')
-
-const ROOT = path.resolve(__dirname, '..')
-const INSTALLER_DIR = path.join(ROOT, 'build', 'windows', 'installer')
-const WINDOWS_DIR = path.join(ROOT, 'build', 'windows')
-const PROBE_DIR = path.join(ROOT, '.smoke', 'nsis-probe')
-const MAKENSIS = process.env.MAKENSIS || 'C:/Users/admin/AppData/Local/nsis-3.10/makensis.exe'
-const APP_EXE = path.join(ROOT, 'build', 'bin', 'starmusic.exe')
-const REAL_INSTALLER = path.join(ROOT, 'build', 'bin', 'starmusic-pc-1.3.0-Setup.exe')
-
-let failed = 0
-const ok = (m) => console.log(`✓ ${m}`)
-const bad = (m) => {
-  console.error(`✗ ${m}`)
-  failed++
-}
-
-function cleanup() {
-  try {
-    fs.rmSync(PROBE_DIR, { recursive: true, force: true })
-  } catch (e) {
-    /* 忽略 */
-  }
-}
-
-// ============================================================
-// A. 源文件编码
-// ============================================================
-console.log('--- A. project.nsi 源文件编码 ---')
-const nsiPath = path.join(INSTALLER_DIR, 'project.nsi')
-if (!fs.existsSync(nsiPath)) {
-  bad('找不到 build/windows/installer/project.nsi')
-} else {
-  const raw = fs.readFileSync(nsiPath)
-  const hasBom = raw[0] === 0xef && raw[1] === 0xbb && raw[2] === 0xbf
-  hasBom ? ok('带 UTF-8 BOM(NSIS 据此按 UTF-8 解码)') : bad('缺少 UTF-8 BOM —— 中文会按 GBK 解码成乱码')
-
-  const body = raw.subarray(3).toString('utf8')
-  const cjk = (body.match(/[\u4e00-\u9fff]/g) || []).length
-  body.includes('\uFFFD')
-    ? bad('内容不是合法 UTF-8(含替换字符)')
-    : ok(`内容为合法 UTF-8,含 ${cjk} 个中文字符`)
-}
-
-// ============================================================
-// B. makensis 的编码识别结果
-// ============================================================
-console.log('\n--- B. makensis 编码识别 ---')
-let makensisReportedUtf8 = false
-if (!fs.existsSync(MAKENSIS)) {
-  bad(`找不到 makensis:${MAKENSIS}`)
-} else if (!fs.existsSync(APP_EXE)) {
-  bad(`找不到待打包 exe:${APP_EXE}`)
-} else {
-  cleanup()
-  fs.mkdirSync(PROBE_DIR, { recursive: true })
-
-  const src = fs.readFileSync(nsiPath).toString('utf8')
-  let nsi = src.replace(
-    '!include "wails_tools.nsh"',
-    '!include "wails_tools.nsh"\n\nSetCompressor zlib'
-  )
-  // 探针目录结构与正式目录不同:MUI_ICON 的 "..\icon.ico" 改为同目录引用
-  nsi = nsi.replace(/!define\s+MUI_ICON\s+"\.\.\\icon\.ico"/, '!define MUI_ICON "icon.ico"')
-  nsi = nsi.replace(/!define\s+MUI_UNICON\s+"\.\.\\icon\.ico"/, '!define MUI_UNICON "icon.ico"')
-  // 产物改到探针目录,避免覆盖正式安装包
-  nsi = nsi.replace(/OutFile\s+"[^"]+"/, `OutFile "${path.join(PROBE_DIR, 'probe.exe')}"`)
-  fs.writeFileSync(path.join(PROBE_DIR, 'project.nsi'), Buffer.from(nsi, 'utf8'))
-  for (const f of ['wails_tools.nsh', 'license.txt']) {
-    const p = path.join(INSTALLER_DIR, f)
-    if (fs.existsSync(p)) fs.copyFileSync(p, path.join(PROBE_DIR, f))
-  }
-  const tmpDir = path.join(INSTALLER_DIR, 'tmp')
-  if (fs.existsSync(tmpDir)) fs.cpSync(tmpDir, path.join(PROBE_DIR, 'tmp'), { recursive: true })
-  fs.copyFileSync(path.join(WINDOWS_DIR, 'icon.ico'), path.join(PROBE_DIR, 'icon.ico'))
-
-  try {
-    const out = execFileSync(
-      MAKENSIS,
-      [`-DARG_WAILS_AMD64_BINARY=${APP_EXE}`, 'project.nsi'],
-      { cwd: PROBE_DIR, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }
-    )
-    const line = out.split(/\r?\n/).find((l) => /Processing script file/.test(l))
-    makensisReportedUtf8 = !!line && /\(UTF8\)/i.test(line)
-    if (makensisReportedUtf8) ok(`makensis 报告:${line.trim()}`)
-    else bad(`makensis 未报告 UTF8:${line || '(无输出)'}`)
-
-    const errs = out.split(/\r?\n/).filter((l) => /^Error|aborting/i.test(l.trim()))
-    if (errs.length) {
-      bad('编译报错:')
-      errs.slice(0, 6).forEach((e) => console.error('    ' + e.trim()))
-    }
-  } catch (e) {
-    bad('探针编译失败')
-    console.error(String(e.stdout || '').split(/\r?\n/).slice(0, 12).join('\n'))
-    console.error(String(e.stderr || '').split(/\r?\n/).slice(0, 12).join('\n'))
-  }
-}
-
-// ============================================================
-// C. 成品中未压缩版本资源的中文比对
-// ============================================================
-console.log('\n--- C. 成品安装包中的中文(PE 版本资源,未压缩)---')
-
-/** 仅来自 project.nsi 的中文断言:[期望在成品中出现的完整字符串, 来源说明] */
-const CLAIMS = [
-  ['星音乐 安装程序', 'FileDescription —— 由 project.nsi 的 INFO_PRODUCTNAME + 字面量拼接'],
-  ['2026 仰望星辰工作室', 'LegalCopyright —— project.nsi 的 INFO_COPYRIGHT'],
-]
-
-if (!fs.existsSync(REAL_INSTALLER)) {
-  bad(`找不到安装包:${REAL_INSTALLER}`)
-} else {
-  const bin = fs.readFileSync(REAL_INSTALLER)
-  console.log(`安装包:${(bin.length / 1024 / 1024).toFixed(1)} MB`)
-
-  for (const [text, from] of CLAIMS) {
-    const needle = Buffer.from(text, 'utf16le')
-    if (bin.includes(needle)) {
-      ok(`命中「${text}」  (${from})`)
-    } else {
-      bad(`未命中「${text}」  (${from}) —— 中文可能在编译环节被错误解码`)
-    }
-  }
-
-  // 反向确认:确认我们对比的确实是从 project.nsi 读到的文本,而非凭空构造
-  const nsiText = fs.readFileSync(nsiPath).subarray(3).toString('utf8')
-  const bothFromSource = CLAIMS.every(([t]) => nsiText.includes(t.split(' ').pop()))
-  bothFromSource
-    ? ok('断言的文案片段均可回溯到 project.nsi 源文件')
-    : bad('断言文案与源文件不匹配(验证脚本自身有问题)')
-}
-
-// ============================================================
-// 汇总
-// ============================================================
-cleanup()
-console.log('')
-if (failed) {
-  console.error(`验证失败:${failed} 项`)
-  process.exit(1)
-}
-console.log('安装包中文编码验证通过:A(BOM/UTF-8)+ B(makensis 按 UTF-8 解码)+ C(成品中文正确)全部成立')