fix(t72): portal 下載頁依作業系統分流(原本寫死「下載 Mac 版」)
leo 07-27 交辦第①件的一部分。B 類(改 Arcrun 本體 UI)→ **待 leo 審**,未合併。
問題:進站完成安裝清單只給「下載 Mac 版」,Windows 用戶按下去拿到 .app 的 zip,
打不開也不知道為什麼。
改動(console-ui/public/portal/index.html):
- 新增 detectOS():優先 navigator.userAgentData.platform(不受 UA 凍結影響),
退回 platform/userAgent;iPadOS 自稱 Macintosh 用 maxTouchPoints 排除。
**認不出來就不猜**,兩個連結都給(同「按了連到錯的機制=不友善」判準)。
- 主按鈕依 OS 換字(下載 Windows 版/Mac 版)+另一系統的小字連結。
- 首開提示依 OS 換:Windows=SmartScreen「其他資訊→仍要執行」白話說明;
Mac=原本的右鍵→打開。
- Windows zip **尚未上鏡像**(交叉編譯已通、真機未驗)→ 沒設 config.daemonDownloadWin
時誠實顯示「Windows 版封測驗證中」,不放 404 連結;Windows 用戶仍可改抓 Mac 版或手動打勾。
- config.js 補註解說明 daemonDownload / daemonDownloadWin 兩個 key。
驗證:console-ui/test/portal-os-split.test.mjs(自帶,node 直接跑)——
8 種 navigator 情境全過(Win Chrome/舊 Firefox、mac Safari/Chrome、iPad、Linux、Android、空值),
並印出三種 OS 各自的實際文案;index.html 主 script 區塊 node --check 語法通過。
⚠️ 未在真瀏覽器點過(沙箱無 portal session),故標待審不標完成。
This commit is contained in:
@@ -1,2 +1,6 @@
|
||||
// Arcrun UI runtime 組態——改這一行就能切 API 目標,不必重新 build。
|
||||
// 可選 key(t72):
|
||||
// daemonDownload ── 同步小幫手 Mac 版 zip 網址(省略=用 index.html 內建的鏡像網址)
|
||||
// daemonDownloadWin ── Windows 版 zip 網址。**真機驗過、zip 上鏡像之後才填**;
|
||||
// 沒填時下載頁會誠實顯示「Windows 版封測驗證中」而不是給一個 404 連結。
|
||||
window.ARCRUN_CONFIG = { apiBase: "https://cypher.arcrun.dev" };
|
||||
|
||||
@@ -670,6 +670,35 @@ function taipeiMonthDay(ms) { var d = new Date(ms + TAIPEI_OFFSET_MS); return {
|
||||
try { localStorage.setItem('arcrun_setup_steps', JSON.stringify(s)); } catch (e) { /* noop */ }
|
||||
renderSetupChecklist();
|
||||
}
|
||||
// ── t72:下載頁依作業系統分流 ──────────────────────────────
|
||||
// 原本寫死「下載 Mac 版」:Windows 用戶按下去會拿到一個 .app 的 zip,打不開也不知道為什麼。
|
||||
// 認不出系統時**不猜**,兩個連結都給(同「按了連到錯的機制=不友善」判準)。
|
||||
function detectOS() {
|
||||
var uad = navigator.userAgentData; // Chromium 系最準(不受 UA 字串凍結影響)
|
||||
var s = (((uad && uad.platform) || navigator.platform || '') + ' ' + (navigator.userAgent || '')).toLowerCase();
|
||||
if (s.indexOf('win') >= 0) return 'win';
|
||||
// iPadOS 會自稱 Macintosh:有多點觸控=平板,不給桌機安裝檔
|
||||
if (s.indexOf('mac') >= 0 && !(navigator.maxTouchPoints > 1)) return 'mac';
|
||||
return 'unknown'; // Linux/手機/認不出來
|
||||
}
|
||||
function osLabel(os) { return os === 'win' ? 'Windows ' : 'Mac '; }
|
||||
function firstOpenHint(os) {
|
||||
return os === 'win'
|
||||
// 未簽章=SmartScreen 必攔一次。不解釋的話用戶會以為是病毒(t72 README 有完整文案)
|
||||
? '(封測版未簽章,Windows 會跳「已保護您的電腦」→ 點「其他資訊」→「仍要執行」)'
|
||||
: '(封測版未簽章,第一次請右鍵→打開)';
|
||||
}
|
||||
function otherOsLine(os, macUrl, winUrl) {
|
||||
var other = os === 'win' ? macUrl : winUrl;
|
||||
var name = os === 'win' ? 'Mac' : 'Windows';
|
||||
if (!other) {
|
||||
return os === 'win' ? ''
|
||||
: '<div class="muted" style="font-size:12.5px">用 Windows?封測驗證中,做好會通知你。</div>';
|
||||
}
|
||||
return '<div class="muted" style="font-size:12.5px">用 ' + name + '?'
|
||||
+ '<a href="' + esc(other) + '">下載 ' + name + ' 版</a></div>';
|
||||
}
|
||||
|
||||
function renderSetupChecklist() {
|
||||
var old = document.getElementById('setup-checklist');
|
||||
if (old) old.remove();
|
||||
@@ -678,7 +707,12 @@ 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 macUrl = cfg.daemonDownload || 'https://raw.githubusercontent.com/youlinhsieh/arcrun-rag-bundles/main/daemon/ArcrunRAG-mac-unsigned.zip';
|
||||
// t72:Windows 版的下載點**還沒上鏡像**(交叉編譯已通、真機未驗)。
|
||||
// 沒設 daemonDownloadWin 就不放連結——按下去 404 比沒有按鈕更傷(同下面「誠實降級」判準)。
|
||||
var winUrl = cfg.daemonDownloadWin || '';
|
||||
var os = detectOS();
|
||||
var daemonUrl = os === 'win' ? winUrl : macUrl;
|
||||
var row = function (done, id, html) {
|
||||
return '<div style="display:flex;gap:10px;align-items:baseline;margin-top:10px">'
|
||||
+ '<span>' + (done ? '✅' : '⬜') + '</span><div style="flex:1">' + html + '</div></div>';
|
||||
@@ -690,12 +724,17 @@ function taipeiMonthDay(ms) { var d = new Date(ms + TAIPEI_OFFSET_MS); return {
|
||||
+ row(s.daemon, 'daemon',
|
||||
'<b>下載同步小幫手</b>(把資料夾變成知識庫)<br>'
|
||||
+ (daemonUrl
|
||||
? '<a href="' + esc(daemonUrl) + '" id="sc-daemon">下載 Mac 版</a>'
|
||||
+ '<span class="muted" style="font-size:12.5px">(封測版未簽章,第一次請右鍵→打開)</span>'
|
||||
? '<a href="' + esc(daemonUrl) + '" id="sc-daemon">下載 ' + osLabel(os) + '版</a>'
|
||||
+ '<span class="muted" style="font-size:12.5px">' + firstOpenHint(os) + '</span>'
|
||||
+ otherOsLine(os, macUrl, winUrl)
|
||||
// 誠實降級(leo 判準:按了連到錯的機制=不友善):沒有可用下載點時
|
||||
// 不放死連結,說明現況並讓這步可手動打勾,不卡住清單。
|
||||
: '<span class="muted" style="font-size:13px">封測期由我們直接寄給你安裝檔(Mac 版);'
|
||||
+ '收到後回來按 <a href="#" id="sc-daemon">我已經裝好了</a>。</span>')
|
||||
: '<span class="muted" style="font-size:13px">'
|
||||
+ (os === 'win'
|
||||
? 'Windows 版還在封測驗證中(做好會寄給你);先用 Mac 的話 '
|
||||
+ '<a href="' + esc(macUrl) + '">下載 Mac 版</a>,'
|
||||
: '封測期由我們直接寄給你安裝檔;')
|
||||
+ '裝好後回來按 <a href="#" id="sc-daemon">我已經裝好了</a>。</span>')
|
||||
// t54(leo:「最好的就是把它的帳密直接輸入」):設定不再是一個要下載的檔案——
|
||||
// 小幫手第一次開啟會問網址+帳密,自己去換設定。
|
||||
+ '<div class="muted" style="font-size:12.5px;margin-top:4px">裝好第一次開啟時,貼上這個網址+你的帳號密碼就連上了,不用下載設定檔。</div>')
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
// t72 下載頁 OS 分流單測(2026-07-27):8 種 navigator 情境 → 分流結果。
|
||||
// 用法:node console-ui/test/portal-os-split.test.mjs
|
||||
// 直接從 index.html 抽最大的 <script> 區塊,測 t72 新增的四個分流函式。
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
const here = path.dirname(fileURLToPath(import.meta.url));
|
||||
const html = fs.readFileSync(path.join(here, '..', 'public', 'portal', 'index.html'), 'utf8');
|
||||
const src = html.match(/<script>([\s\S]*?)<\/script>/g)
|
||||
.map((b) => b.replace(/^<script>|<\/script>$/g, ''))
|
||||
.sort((a, b) => b.length - a.length)[0];
|
||||
const pick = (name) => {
|
||||
const i = src.indexOf('function ' + name + '(');
|
||||
if (i < 0) throw new Error('找不到 ' + name);
|
||||
let depth = 0, j = src.indexOf('{', i);
|
||||
for (let k = j; k < src.length; k++) {
|
||||
if (src[k] === '{') depth++;
|
||||
else if (src[k] === '}') { depth--; if (!depth) return src.slice(i, k + 1); }
|
||||
}
|
||||
};
|
||||
const code = ['detectOS','osLabel','firstOpenHint','otherOsLine'].map(pick).join('\n');
|
||||
const esc = (s) => String(s).replace(/&/g,'&').replace(/</g,'<').replace(/"/g,'"');
|
||||
const make = (nav) => {
|
||||
const fn = new Function('navigator','esc', code + '; return {detectOS,osLabel,firstOpenHint,otherOsLine};');
|
||||
return fn(nav, esc);
|
||||
};
|
||||
const MAC='https://x/mac.zip', WIN='https://x/win.zip';
|
||||
const cases = [
|
||||
['Windows 11 Chrome', {userAgentData:{platform:'Windows'},platform:'Win32',userAgent:'Mozilla/5.0 (Windows NT 10.0; Win64; x64)',maxTouchPoints:0}, 'win'],
|
||||
['Windows 舊 Firefox(無 userAgentData)', {platform:'Win32',userAgent:'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Firefox/128',maxTouchPoints:0}, 'win'],
|
||||
['macOS Safari', {platform:'MacIntel',userAgent:'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) Safari/605',maxTouchPoints:0}, 'mac'],
|
||||
['macOS Chrome (UA-CH)', {userAgentData:{platform:'macOS'},platform:'MacIntel',userAgent:'Mozilla/5.0 (Macintosh) Chrome/141',maxTouchPoints:0}, 'mac'],
|
||||
['iPad(自稱 Macintosh,有觸控)', {platform:'MacIntel',userAgent:'Mozilla/5.0 (Macintosh; Intel Mac OS X) Safari',maxTouchPoints:5}, 'unknown'],
|
||||
['Linux', {platform:'Linux x86_64',userAgent:'Mozilla/5.0 (X11; Linux x86_64)',maxTouchPoints:0}, 'unknown'],
|
||||
['Android', {platform:'Linux armv8l',userAgent:'Mozilla/5.0 (Linux; Android 14)',maxTouchPoints:5}, 'unknown'],
|
||||
['什麼都沒有', {maxTouchPoints:0}, 'unknown'],
|
||||
];
|
||||
let fail = 0;
|
||||
for (const [name, nav, want] of cases) {
|
||||
const m = make(nav);
|
||||
const got = m.detectOS();
|
||||
const ok = got === want;
|
||||
if (!ok) fail++;
|
||||
console.log(`${ok?'✅':'❌'} ${name.padEnd(34)} → ${got} (期望 ${want})`);
|
||||
}
|
||||
console.log('\n--- 各情境實際顯示的文字 ---');
|
||||
for (const os of ['win','mac','unknown']) {
|
||||
const m = make({});
|
||||
console.log(`\n[${os}] 主按鈕:下載 ${m.osLabel(os)}版`);
|
||||
console.log(` 首開提示:${m.firstOpenHint(os)}`);
|
||||
console.log(` 另一個系統(Win zip 已上架):${m.otherOsLine(os, MAC, WIN) || '(不顯示)'}`);
|
||||
console.log(` 另一個系統(Win zip 未上架=現況):${m.otherOsLine(os, MAC, '') || '(不顯示)'}`);
|
||||
}
|
||||
process.exit(fail ? 1 : 0);
|
||||
Reference in New Issue
Block a user