插件必须填 NAVI_SYNC_TOKEN,但页面上没有任何地方能看到它 —— 用户只能去翻 .env,实际就是配不起来。 - admin-server 新增 GET /admin/api/sync-info,返回 syncToken / hasToken。 放在 /admin/ 下而不是 /api/ 下是必须的:插件上报用的 /api/history/sync 被门户 auth_request 放行,若令牌挂在同一前缀下就等于公开。 实测 /api/history/sync-info 返回 401,只有 /admin/api/sync-info 能取到。 - 安装引导页把「第一步」改为填写两项(服务地址 + 同步令牌),各自可复制; 服务端未设令牌时提示留空即可;未登录时提示需先登录门户。 - README 补充对应说明。
290 lines
13 KiB
HTML
290 lines
13 KiB
HTML
<!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="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>
|