From 53c6334fd98c2884e028513d41f376bd6bbbd05a Mon Sep 17 00:00:00 2001 From: uncle6me-web Date: Mon, 27 Jul 2026 19:28:43 +0800 Subject: [PATCH] =?UTF-8?q?fix(portal/t72):=20=E4=B8=8B=E8=BC=89=E9=A0=81?= =?UTF-8?q?=20OS=20=E5=88=86=E6=B5=81=E2=80=94=E2=80=94Windows=20=E5=AE=A2?= =?UTF-8?q?=E6=88=B6=E4=B8=8D=E5=86=8D=E6=8B=BF=E5=88=B0=20Mac=20=E7=9A=84?= =?UTF-8?q?=20.app?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit leo 07-27:「客戶是用 windows 的」。此前四處寫死「下載 Mac 版」, Windows 用戶按下去拿到 .app=按了連到錯的東西(leo 判準:那不算友善)。 改法(施工圖 rag-wave1/windows-build-and-os-split.md §3): - daemonPick() 依 UA 判 OS,四處共用;判不出來=兩個都給,不替用戶猜 - 判對了也附「不是這個系統?」另一版連結(UA 會判錯,用戶要有路走) - 擋關話術跟著 OS 走:Mac=右鍵打開/Windows=更多資訊→仍要執行 - daemonBase 新 key,保留 daemonDownload 舊 key 相容(由檔名推目錄) - 一律走 raw:Mac zip 21MB > jsDelivr 20MB 上限(實測回 File size exceeded) 測試 os-split.test.mjs 10/10: ⚠️ 測試抓到真 bug——iPhone 的 UA 含 'Mac OS X' 會被判成 Mac, 讓手機用戶下載裝不起來的桌面 app。已加 isMobile 排除,手機落到「兩個都給」。 未驗:真 Windows 機器的實際下載與安裝行為(需真機)。 --- console-ui/public/portal/index.html | 81 ++++++++++++++++++++-- console-ui/public/portal/os-split.test.mjs | 49 +++++++++++++ 2 files changed, 123 insertions(+), 7 deletions(-) create mode 100644 console-ui/public/portal/os-split.test.mjs diff --git a/console-ui/public/portal/index.html b/console-ui/public/portal/index.html index fa5f21c..c47c0e1 100644 --- a/console-ui/public/portal/index.html +++ b/console-ui/public/portal/index.html @@ -667,13 +667,70 @@ function taipeiMonthDay(ms) { var d = new Date(ms + TAIPEI_OFFSET_MS); return { }) .catch(function (e) { $('login-submit').disabled = false; $('login-status').textContent = friendlyErr(e); }); } + // t72 OS 分流(2026-07-27,leo:「客戶是用 windows 的」)——在此之前四處寫死「下載 Mac 版」, + // Windows 客戶按下去會拿到 Mac 的 .app=按了連到錯的東西(leo 判準:那不算友善)。 + // 判不出 OS 時**兩個都給**,不替用戶猜;且無論判成哪個,頁面都留「不是這個系統?」的另一版連結 + // ——UA 會判錯,判錯時用戶要有路可走(施工圖 §3 注意事項 1、3)。 + var DAEMON_BASE_DEFAULT = 'https://raw.githubusercontent.com/youlinhsieh/arcrun-rag-bundles/main/daemon/'; + var DAEMON_MAC = 'ArcrunRAG-mac-unsigned.zip'; + var DAEMON_WIN = 'ArcrunRAG-win-unsigned.zip'; + // Mac 那顆 21MB > jsDelivr 單檔 20MB 上限(實測回 "File size exceeded...")→ 一律走 raw; + // Windows 13MB 雖在限內,同走 raw 保持單一來源、少一個會壞的地方。 + function daemonBase() { + var cfg = (window.ARCRUN_CONFIG || {}); + if (cfg.daemonBase) return String(cfg.daemonBase).replace(/\/?$/, '/'); + // 舊 key 相容(施工圖 §3 注意事項 2):daemonDownload 是「單一檔案網址」, + // 有設就尊重它當 Mac 版網址,並由它推出目錄給 Windows 版用。 + if (cfg.daemonDownload) { + var s = String(cfg.daemonDownload); + return s.slice(0, s.lastIndexOf('/') + 1) || DAEMON_BASE_DEFAULT; + } + return DAEMON_BASE_DEFAULT; + } + function daemonPick() { + var base = daemonBase(); + var ua = navigator.userAgent || ''; + // 手機/平板先排除:iOS 的 UA 含 "Mac OS X",不排除會把 iPhone 判成 Mac, + // 讓手機用戶下載一個裝不起來的桌面 app(測到才發現,2026-07-27)。 + // 手機=判不出桌面 OS → 落到「兩個都給」,用戶回桌機再選。 + var isMobile = /iPhone|iPad|iPod|Android|Mobile/i.test(ua); + var isWin = !isMobile && /Windows NT/i.test(ua); + var isMac = !isMobile && /Macintosh|Mac OS X/i.test(ua) && !/Windows/i.test(ua); + var mac = { os: 'mac', label: '下載 Mac 版', url: base + DAEMON_MAC }; + var win = { os: 'win', label: '下載 Windows 版', url: base + DAEMON_WIN }; + if (isWin) return { pick: win, other: mac, sure: true }; + if (isMac) return { pick: mac, other: win, sure: true }; + return { pick: null, other: null, sure: false, mac: mac, win: win }; + } + // 封測期第一次開啟的擋關提示(未簽章)——Mac/Windows 攔法不同,話術也不同。 + function daemonHint(os) { + if (os === 'win') return '(封測版未簽章,Windows 第一次會跳藍色視窗擋下來——點「更多資訊」→「仍要執行」就好)'; + return '(封測版未簽章,第一次請右鍵→打開)'; + } // 07-27:設定頁的常駐入口(下載小幫手/換 AI 金鑰)——與一次性卡片同一組 API (function () { var dl = $('st-daemon-dl'); if (dl) { - var u = (window.ARCRUN_CONFIG && window.ARCRUN_CONFIG.daemonDownload) - || 'https://raw.githubusercontent.com/youlinhsieh/arcrun-rag-bundles/main/daemon/ArcrunRAG-mac-unsigned.zip'; - dl.setAttribute('href', u); + var d = daemonPick(); + if (d.sure) { + dl.setAttribute('href', d.pick.url); + dl.textContent = d.pick.label; + // 判對了也要留另一版的路(UA 會判錯) + var alt = document.createElement('span'); + alt.className = 'muted'; + alt.style.cssText = 'font-size:12.5px;margin-left:8px'; + alt.innerHTML = '不是這個系統?' + d.other.label + ''; + if (dl.parentNode) dl.parentNode.insertBefore(alt, dl.nextSibling); + } else { + // 判不出來=兩個都給,不預設 Mac + dl.setAttribute('href', d.mac.url); + dl.textContent = d.mac.label; + var both = document.createElement('span'); + both.className = 'muted'; + both.style.cssText = 'font-size:12.5px;margin-left:8px'; + both.innerHTML = '或 ' + d.win.label + ''; + if (dl.parentNode) dl.parentNode.insertBefore(both, dl.nextSibling); + } } var kb = $('st-key-save'); if (kb) kb.addEventListener('click', function () { @@ -719,7 +776,8 @@ function taipeiMonthDay(ms) { var d = new Date(ms + TAIPEI_OFFSET_MS); return { var s = setupSteps(); if (s.daemon && s.key) return; // t54 起只剩兩件:設定改由小幫手輸入帳密自取,不再下載檔案 var cfg = window.ARCRUN_CONFIG || {}; - var daemonUrl = cfg.daemonDownload || 'https://raw.githubusercontent.com/youlinhsieh/arcrun-rag-bundles/main/daemon/ArcrunRAG-mac-unsigned.zip'; + var dpick = daemonPick(); // t72 OS 分流(同一組判定,見上面 daemonPick) + var daemonUrl = dpick.sure ? dpick.pick.url : dpick.mac.url; var row = function (done, id, html) { return '
' + '' + (done ? '✅' : '⬜') + '
' + html + '
'; @@ -731,11 +789,20 @@ function taipeiMonthDay(ms) { var d = new Date(ms + TAIPEI_OFFSET_MS); return { + row(s.daemon, 'daemon', '下載同步小幫手(把資料夾變成知識庫)
' + (daemonUrl - ? '下載 Mac 版' - + '(封測版未簽章,第一次請右鍵→打開)' + // t72:按鈕字樣+擋關話術都跟著 OS 走;並且一律附「另一個系統」的連結 + //(UA 判錯時用戶要有路走;判不出來時=兩個平等並列,不預設 Mac) + ? '' + + esc(dpick.sure ? dpick.pick.label : dpick.mac.label) + '' + + '' + + esc(daemonHint(dpick.sure ? dpick.pick.os : 'mac')) + '' + + '
' + + (dpick.sure + ? '不是這個系統?' + esc(dpick.other.label) + '' + : '或 ' + esc(dpick.win.label) + '') + + '
' // 誠實降級(leo 判準:按了連到錯的機制=不友善):沒有可用下載點時 // 不放死連結,說明現況並讓這步可手動打勾,不卡住清單。 - : '封測期由我們直接寄給你安裝檔(Mac 版);' + : '封測期由我們直接寄給你安裝檔;' + '收到後回來按 我已經裝好了') // t54(leo:「最好的就是把它的帳密直接輸入」):設定不再是一個要下載的檔案—— // 小幫手第一次開啟會問網址+帳密,自己去換設定。 diff --git a/console-ui/public/portal/os-split.test.mjs b/console-ui/public/portal/os-split.test.mjs new file mode 100644 index 0000000..3075676 --- /dev/null +++ b/console-ui/public/portal/os-split.test.mjs @@ -0,0 +1,49 @@ +import fs from 'node:fs'; +const html = fs.readFileSync(new URL('./index.html', import.meta.url).pathname,'utf8'); +// 抽出 daemonPick 相關函式(從 DAEMON_BASE_DEFAULT 到 daemonHint 結尾) +const start = html.indexOf('var DAEMON_BASE_DEFAULT'); +const endMark = "return '(封測版未簽章,第一次請右鍵→打開)';\n }"; +const end = html.indexOf(endMark) + endMark.length; +if (start < 0 || end < start) throw new Error('抽不到函式區塊'); +const src = html.slice(start, end); + +const cases = [ + ['Windows', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120 Safari/537.36'], + ['Mac', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 Safari/605.1.15'], + ['iPhone', 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 Safari/604.1'], + ['Linux', 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/120 Safari/537.36'], +]; +let pass=0, fail=0; +const chk=(l,c,extra='')=>{ if(c){console.log('PASS:',l);pass++;} else {console.log('FAIL:',l,extra);fail++;} }; + +for (const [name, ua] of cases) { + const fn = new Function('navigator','window', src + '; return {daemonPick:daemonPick, daemonHint:daemonHint, daemonBase:daemonBase};'); + const api = fn({userAgent: ua}, {}); + const d = api.daemonPick(); + const label = d.sure ? d.pick.label : '(兩個都給)'; + const url = d.sure ? d.pick.url : d.mac.url + ' + ' + d.win.url; + console.log(`\n[${name}] sure=${d.sure} → ${label}`); + console.log(` url: ${url}`); + if (name==='Windows') { + chk('Windows 給 win zip', d.sure && d.pick.url.endsWith('ArcrunRAG-win-unsigned.zip'), d.pick&&d.pick.url); + chk('Windows 另一版是 Mac', d.other && d.other.url.endsWith('mac-unsigned.zip')); + chk('Windows 話術提 藍色視窗', api.daemonHint('win').includes('仍要執行')); + } + if (name==='Mac') { + chk('Mac 給 mac zip', d.sure && d.pick.url.endsWith('ArcrunRAG-mac-unsigned.zip')); + chk('Mac 另一版是 Windows', d.other && d.other.url.endsWith('win-unsigned.zip')); + chk('Mac 話術提 右鍵打開', api.daemonHint('mac').includes('右鍵')); + } + if (name==='iPhone' || name==='Linux') { + // iPhone 含 "Mac OS X" 但不是桌機 Mac;Linux 兩者皆非 → 都該落在「不確定=兩個都給」 + if (name==='Linux') chk('Linux 判不出來→兩個都給', d.sure===false); + if (name==='iPhone') chk('iPhone 不該被判成 Mac(手機→兩個都給)', d.sure===false, 'sure='+d.sure); + } +} +// 舊 key 相容 +const fn2 = new Function('navigator','window', src + '; return daemonBase();'); +console.log('\n[相容] daemonDownload 舊 key →', fn2({userAgent:''},{ARCRUN_CONFIG:{daemonDownload:'https://x.dev/d/ArcrunRAG-mac-unsigned.zip'}})); +chk('舊 key 推得出目錄', fn2({userAgent:''},{ARCRUN_CONFIG:{daemonDownload:'https://x.dev/d/ArcrunRAG-mac-unsigned.zip'}})==='https://x.dev/d/'); +chk('daemonBase 新 key 優先', fn2({userAgent:''},{ARCRUN_CONFIG:{daemonBase:'https://y.dev/z'}})==='https://y.dev/z/'); +console.log(`\n=== ${pass} passed, ${fail} failed ===`); +process.exit(fail?1:0);