Files
HomeDash/web/extension/install.html
JianFeeeee 9678c9e01b docs(extension): 安装页说明「未签名」拒绝的正解(用户实测踩到)
正式版 Firefox 在 about:addons 的「从文件安装」强制要求 Mozilla 签名,
自签 xpi 会报「附加组件未签名 / not verified」。唯一免签路径是
about:debugging#/runtime/this-firefox 的「临时加载附加组件」。

同时说明临时加载的代价:浏览器重启即失效,而这个扩展靠 alarms 每 30 秒
同步历史、必须常驻,所以要长期用得上必须走 AMO 签名发布。
2026-10-02 15:56:58 +08:00

301 lines
14 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="data:,">
<title>安装浏览历史插件</title>
<style>
*{box-sizing:border-box;margin:0;padding:0}
/* .btn 自己设了 display,会盖掉浏览器默认的 [hidden]{display:none},
导致未配置商店时「一键安装」按钮仍然可见且 href 为空 —— 必须显式兜住 */
[hidden]{display:none !important}
body{font-family:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",sans-serif;
background:
radial-gradient(1000px 500px at 10% -10%, #1e3a8a 0%, transparent 60%),
radial-gradient(900px 500px at 90% 0%, #312e81 0%, transparent 55%),
#0f172a;
background-attachment:fixed;color:#e2e8f0;line-height:1.6;padding:32px 18px 64px}
.wrap{max-width:760px;margin:0 auto}
h1{font-size:24px;font-weight:700;margin-bottom:6px}
.sub{color:#94a3b8;font-size:14px;margin-bottom:24px}
.card{background:rgba(30,41,59,.72);border:1px solid #334155;border-radius:16px;
padding:20px 22px;margin-bottom:16px;
backdrop-filter:blur(14px) saturate(150%);-webkit-backdrop-filter:blur(14px) saturate(150%);
box-shadow:0 10px 30px rgba(0,0,0,.28)}
.card h2{font-size:17px;font-weight:600;margin-bottom:12px;display:flex;align-items:center;gap:10px;flex-wrap:wrap}
.badge{font-size:12px;font-weight:600;padding:2px 9px;border-radius:999px;
background:#1e3a5f;color:#93c5fd;border:1px solid #2563eb;white-space:nowrap}
.badge.warn{background:#3f2d17;color:#fcd34d;border-color:#b45309}
.card.current{border-color:#2563eb;box-shadow:0 0 0 1px #2563eb inset,0 10px 30px rgba(0,0,0,.28)}
ol{margin:0 0 0 20px;font-size:14px;color:#cbd5e1}
ol li{margin-bottom:7px}
a{color:#60a5fa}
code{background:#0b1220;border:1px solid #334155;border-radius:5px;padding:1px 6px;font-size:13px}
.note{font-size:13px;color:#94a3b8;margin-top:12px;border-top:1px solid #334155;padding-top:12px}
.actions{display:flex;flex-wrap:wrap;gap:10px;margin-top:14px}
.btn{display:inline-block;padding:9px 18px;border-radius:10px;font-size:14px;font-weight:600;
text-decoration:none;border:1px solid #334155;background:#0b1220;color:#e2e8f0;cursor:pointer}
.btn.primary{background:#2563eb;border-color:#2563eb;color:#fff}
.btn.primary:hover{background:#1d4ed8}
.site{display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin-top:10px}
.site input{flex:1;min-width:240px;background:#0b1220;border:1px solid #334155;border-radius:10px;
padding:9px 12px;color:#e2e8f0;font-size:14px}
.hint{font-size:13px;color:#94a3b8;margin-top:10px}
.lbl{display:block;font-size:12px;font-weight:600;color:#94a3b8;margin:12px 0 0}
.done{font-size:13px;color:#86efac;margin-top:8px}
</style>
</head>
<body>
<div class="wrap">
<h1>安装浏览历史插件</h1>
<p class="sub">安装后浏览记录会自动同步到本站,并据此把最常访问的服务排到「常用服务」最前面。</p>
<div class="card">
<h2>第一步:填写插件设置</h2>
<p class="hint">在插件设置里填下面两项,各自点「复制」再粘贴。</p>
<label class="lbl" for="site-url">服务地址</label>
<div class="site">
<input id="site-url" readonly>
<button class="btn" id="copy-btn" type="button">复制</button>
</div>
<label class="lbl" for="site-token">同步令牌</label>
<div class="site">
<input id="site-token" readonly placeholder="读取中…">
<button class="btn" id="copy-token" type="button">复制</button>
</div>
<p class="hint" id="token-note"></p>
<div class="done" id="copy-done" hidden>已复制</div>
</div>
<div class="card" id="card-chromium" hidden>
<h2>Chrome / Edge / 其他 Chromium <span class="badge">已识别你的浏览器</span></h2>
<div class="actions">
<a class="btn primary" id="store-chromium" hidden>从商店一键安装</a>
<a class="btn" href="/extension/navi-chrome.zip" download id="dl-chromium">下载扩展包(zip)</a>
</div>
<ol>
<li>解压下载的 zip 到一个固定目录(不要删除)</li>
<li>地址栏打开 <code id="ext-url">chrome://extensions</code></li>
<li>打开右上角「开发者模式」</li>
<li>点「加载已解压的扩展程序」,选择解压出来的目录</li>
<li>点扩展图标,填入上面的本站地址与同步令牌,保存</li>
</ol>
<p class="note" id="note-chromium"></p>
</div>
<div class="card" id="card-firefox" hidden>
<h2>Firefox <span class="badge">已识别你的浏览器</span></h2>
<div class="actions">
<a class="btn primary" id="store-firefox" hidden>从商店一键安装</a>
<a class="btn" href="/extension/navi-firefox.xpi" download>下载扩展包(xpi)</a>
</div>
<ol>
<li>地址栏打开 <code>about:debugging#/runtime/this-firefox</code></li>
<li>点「临时加载附加组件」,选择下载的 xpi</li>
<li>点扩展图标,填入上面的本站地址与同步令牌,保存</li>
</ol>
<p class="hint" style="margin-top:.5rem">
<b>不要在 <code>about:addons</code> 里点「从文件安装」</b>。本扩展是自签名(未上架
任何商店),那条路在正式版 Firefox 会因缺少 Mozilla 签名而报
「附加组件未签名 / not verified」。只有 <code>about:debugging</code> 的
「临时加载附加组件」不需要签名。
</p>
<p class="hint">
注意:<b>临时加载的扩展在浏览器重启后会被移除</b>,需要每次重启后重新加载一次。
若要长期常驻,需把 xpi 提交到 addons.mozilla.org 签名后发布,
或改用 Chromium 版(zip 需开发者模式,但同样不需签名)。
</p>
<p class="note" id="note-firefox"></p>
</div>
<div class="card" id="card-mobile" hidden>
<h2>手机浏览器 <span class="badge warn">此环境无法安装扩展</span></h2>
<p class="hint">iOS 上所有浏览器都只能用 Safari 内核,且 Safari 扩展需要 Xcode 打包的 App 才能安装;
Android 版 Chrome 也不支持安装扩展。所以手机上无法完成这一步。</p>
<div class="actions">
<a class="btn" href="/extension/navi-chrome.zip" download>下载 Chromium 版(zip)</a>
<a class="btn" href="/extension/navi-firefox.xpi" download>下载 Firefox 版(xpi)</a>
</div>
<p class="note">请在电脑上打开本页面完成安装。手机上浏览产生的记录暂不会同步,但电脑端安装后仍会统计。</p>
</div>
<div class="card" id="card-other" hidden>
<h2>其他浏览器</h2>
<p class="hint">未能识别浏览器内核。Chromium 系(Brave、Vivaldi、Opera、Kiwi 等)请按上面 Chrome 的步骤操作;
Firefox 系请按 Firefox 的步骤操作。</p>
<div class="actions">
<a class="btn" href="/extension/navi-chrome.zip" download>下载 Chromium 版(zip)</a>
<a class="btn" href="/extension/navi-firefox.xpi" download>下载 Firefox 版(xpi)</a>
</div>
</div>
<div class="card">
<h2>装好后</h2>
<ol>
<li>回到导航页并刷新,本次会话浏览几条页面</li>
<li>约 30 秒后历史会自动上报,「历史访问」开始按访问频次排序</li>
<li>检测到插件已安装后,本入口会自动隐藏</li>
</ol>
</div>
</div>
<script>
(function () {
'use strict';
document.getElementById('site-url').value = location.origin;
var doneTimer = null;
function flashDone() {
var done = document.getElementById('copy-done');
done.hidden = false;
if (doneTimer) clearTimeout(doneTimer);
doneTimer = setTimeout(function () { done.hidden = true; }, 2000);
}
function copyFrom(inputId) {
var input = document.getElementById(inputId);
if (!input || !input.value) return;
input.select();
input.setSelectionRange(0, 999);
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(input.value).then(flashDone, function () {
document.execCommand('copy'); flashDone();
});
} else {
document.execCommand('copy'); flashDone();
}
}
document.getElementById('copy-btn').addEventListener('click', function () { copyFrom('site-url'); });
document.getElementById('copy-token').addEventListener('click', function () { copyFrom('site-token'); });
/* 同步令牌:由 admin-server 提供(/admin/ 在门户登录墙之内,
所以令牌不会出现在被门户放行的 /api/ 路径上)。 */
fetch('/admin/api/sync-info', { cache: 'no-store' })
.then(function (r) { return r.ok ? r.json() : null; })
.then(function (d) {
var tk = document.getElementById('site-token');
var note = document.getElementById('token-note');
if (d && d.hasToken) {
tk.value = d.syncToken;
note.textContent = '把这两项填进插件设置即可。';
} else if (d) {
tk.value = '';
tk.placeholder = '(服务端未设置令牌)';
document.getElementById('copy-token').hidden = true;
note.textContent = '服务端未配置 NAVI_SYNC_TOKEN,令牌留空即可。';
} else {
tk.placeholder = '无法读取,请确认已登录';
note.textContent = '需要登录门户后才能读取令牌。';
}
})
.catch(function () {
document.getElementById('site-token').placeholder = '无法读取,请确认已登录';
});
/* ── 浏览器识别 ──
UA 会互相包含(Edge/Brave/Opera 的 UA 里都有 Chrome/),所以要按
「更具体的先判」的顺序来。
另外 iOS 上所有浏览器都是 WebKit 内核,且 Safari 扩展必须由 App 打包,
根本装不了 —— 必须单独识别,否则会给用户一套做不到的桌面步骤。 */
function detect() {
var ua = navigator.userAgent;
var uad = navigator.userAgentData || null;
var brands = (uad && uad.brands ? uad.brands : []).map(function (b) { return b.brand; });
var brandStr = brands.join(' ');
var isIOS = /iPad|iPhone|iPod/.test(ua) ||
(/Macintosh/.test(ua) && navigator.maxTouchPoints > 1);
var isAndroid = /Android/i.test(ua);
var isFirefox = /firefox\/|fxios\//i.test(ua);
var isEdge = /edg\//i.test(ua) || brands.indexOf('Microsoft Edge') >= 0;
var isOpera = /opr\//i.test(ua) || brands.indexOf('Opera') >= 0;
var isBrave = !!(navigator.brave && navigator.brave.isBrave);
var isVivaldi = /vivaldi/i.test(ua);
var isChromium = !isFirefox && (
/chrome\/|chromium\//i.test(ua) ||
brands.some(function (b) { return /Chromium|Google Chrome/i.test(b); })
);
return {
isIOS: isIOS, isAndroid: isAndroid,
isMobile: isIOS || isAndroid || /Mobile|Tablet/i.test(ua),
isFirefox: isFirefox, isEdge: isEdge, isOpera: isOpera,
isBrave: isBrave, isVivaldi: isVivaldi, isChromium: isChromium,
isSafari: /safari\//i.test(ua) && !isChromium && !isFirefox,
brand: brandStr
};
}
var d = detect();
var card;
if (d.isFirefox && !d.isIOS) {
card = document.getElementById('card-firefox');
} else if (d.isMobile) {
card = document.getElementById('card-mobile');
} else if (d.isChromium) {
card = document.getElementById('card-chromium');
var extUrl = 'chrome://extensions';
var label = 'Chrome';
if (d.isEdge) { extUrl = 'edge://extensions'; label = 'Edge'; }
else if (d.isBrave) { extUrl = 'brave://extensions'; label = 'Brave'; }
else if (d.isVivaldi) { extUrl = 'vivaldi://extensions'; label = 'Vivaldi'; }
else if (d.isOpera) { extUrl = 'opera://extensions'; label = 'Opera'; }
document.getElementById('ext-url').textContent = extUrl;
if (label !== 'Chrome') {
document.querySelector('#card-chromium h2').firstChild.textContent =
label + '(Chromium 内核) ';
}
} else {
card = document.getElementById('card-other');
}
card.hidden = false;
card.classList.add('current');
/* 商店链接是可选的:部署方在 data 卷放 extension-store.json 才启用。
本实例没有发布到任何商店,所以默认把按钮整个移除 —— 避免出现
点不动的「一键安装」。 */
fetch('/extension/store.json', { cache: 'no-store' })
.then(function (r) { return r.ok ? r.json() : null; })
.then(function (s) {
var hasChrome = s && s.chrome;
var hasFirefox = s && s.firefox;
if (hasChrome) showStore('store-chromium', s.chrome);
if (hasFirefox) showStore('store-firefox', s.firefox);
setNotes(hasChrome, hasFirefox);
})
.catch(function () { setNotes(false, false); });
function showStore(id, url) {
var el = document.getElementById(id);
if (!el) return;
el.href = url;
el.hidden = false;
}
function setNotes(fromChromeStore, fromFirefoxStore) {
var c = document.getElementById('note-chromium');
var f = document.getElementById('note-firefox');
if (c) {
c.textContent = fromChromeStore
? '也可以从上面的商店按钮一键安装。'
: '本实例未发布到浏览器商店,Chrome/Edge 不允许从网站直接安装非商店扩展,因此只能按上面步骤手动加载。';
}
if (f) {
f.textContent = fromFirefoxStore
? '也可以从上面的商店按钮一键安装。'
: '本实例未发布到商店;Firefox 正式版只接受已签名的附加组件,未签名的 xpi 只能在开发者版 / ESR 上加载,或在 about:config 关闭 xpinstall.signatures.required。';
}
}
})();
</script>
</body>
</html>