Retire ACP/Hermes/OpenCode surfaces and rename hermes_tools to mnote_agent_tools so Page AI stays on Pi Lab only. Add Chrome vault extension + extension token route, pre-release purge design, and soft-retire legacy smokes for the small-group production cut.
172 lines
5.3 KiB
JavaScript
172 lines
5.3 KiB
JavaScript
import { normalizeBaseUrl, vaultFetch } from "./lib/api.js";
|
||
import { loadConfig, loadPersistent } from "./lib/storage.js";
|
||
|
||
const $ = (id) => document.getElementById(id);
|
||
|
||
function setStatus(text, kind) {
|
||
const el = $("status");
|
||
el.textContent = text || "";
|
||
el.className = kind || "";
|
||
}
|
||
|
||
function setWorkspaceHint(rootUri, rootPath, source) {
|
||
const el = $("workspaceHint");
|
||
if (!el) return;
|
||
if (!rootUri) {
|
||
el.textContent = "";
|
||
return;
|
||
}
|
||
const path = rootPath || rootUri.replace(/^file:\/\//, "");
|
||
const src =
|
||
source === "default"
|
||
? "账号默认工作区"
|
||
: source === "override"
|
||
? "手动覆盖"
|
||
: "已保存";
|
||
el.innerHTML = `密码箱写入:<code>${path}</code>(${src})`;
|
||
}
|
||
|
||
/**
|
||
* 必须在用户点击手势内调用:MV3 从 service worker 弹权限常被静默拒绝,
|
||
* 外网 baseUrl(含非标端口 frp)没有 host 权限时 fetch 就是 Failed to fetch。
|
||
* 同时申请 http(s)://*/*,否则外站 Cookie 读不到、登录态无法保存。
|
||
* @param {string} baseUrl
|
||
*/
|
||
async function requestBaseUrlPermission(baseUrl) {
|
||
const origin = new URL(normalizeBaseUrl(baseUrl)).origin;
|
||
// Broad first so cookie capture works on every login site after connect.
|
||
const broad = ["http://*/*", "https://*/*", `${origin}/*`];
|
||
try {
|
||
const haveBroad = await chrome.permissions.contains({ origins: broad });
|
||
if (haveBroad) return true;
|
||
const ok = await chrome.permissions.request({ origins: broad });
|
||
if (ok) return true;
|
||
} catch {
|
||
/* fall through to narrow */
|
||
}
|
||
const origins = [`${origin}/*`];
|
||
const have = await chrome.permissions.contains({ origins });
|
||
if (have) return true;
|
||
try {
|
||
return await chrome.permissions.request({ origins });
|
||
} catch (e) {
|
||
throw new Error(
|
||
`无法申请访问权限 ${origin}:${e?.message || e}(请在扩展详情里允许该站点;登录态还需「所有网站」)`
|
||
);
|
||
}
|
||
}
|
||
|
||
function formatFetchError(e, baseUrl) {
|
||
const msg = e?.message || String(e);
|
||
if (/Failed to fetch|NetworkError|Load failed/i.test(msg)) {
|
||
return (
|
||
`无法访问 ${baseUrl || "baseUrl"}:${msg}。` +
|
||
`常见原因:① 未点允许扩展访问该站点 ② 外网隧道未通 ③ 证书不受信任。` +
|
||
`请重新加载扩展后再点「测试连接」,并允许弹窗中的站点权限。`
|
||
);
|
||
}
|
||
return msg;
|
||
}
|
||
|
||
async function hydrate() {
|
||
const p = await loadPersistent();
|
||
if (p.baseUrl) $("baseUrl").value = p.baseUrl;
|
||
// 默认不展示手填;仅当用户曾覆盖时回填并展开
|
||
if (p.rootUri) {
|
||
$("rootUri").value = p.rootUri;
|
||
const adv = $("advRoot");
|
||
if (adv && p.rootUri) {
|
||
// 不自动展开;连接后 hint 会显示实际路径
|
||
}
|
||
}
|
||
if (p.email) $("email").value = p.email;
|
||
const cfg = await loadConfig();
|
||
if (cfg.connected) {
|
||
setStatus(
|
||
`已连接 · ${cfg.email || cfg.userId || ""} · token 有效`,
|
||
"ok"
|
||
);
|
||
setWorkspaceHint(cfg.rootUri || p.rootUri, p.rootPath, "saved");
|
||
} else if (cfg.baseUrl) {
|
||
setStatus("已保存配置,但 token 缺失(浏览器重启后需重新登录)", "err");
|
||
setWorkspaceHint(p.rootUri, p.rootPath, "saved");
|
||
}
|
||
}
|
||
|
||
$("test").onclick = async () => {
|
||
setStatus("测试中…");
|
||
let baseUrl = "";
|
||
try {
|
||
baseUrl = normalizeBaseUrl($("baseUrl").value);
|
||
const ok = await requestBaseUrlPermission(baseUrl);
|
||
if (!ok) {
|
||
setStatus(
|
||
`已取消访问权限:需要允许扩展访问 ${new URL(baseUrl).origin}`,
|
||
"err"
|
||
);
|
||
return;
|
||
}
|
||
// whoami 不依赖 /api/health(外网入口可能未挂 health)
|
||
const data = await vaultFetch(baseUrl, "/api/auth/whoami", {
|
||
method: "GET",
|
||
credentials: true,
|
||
});
|
||
setStatus(
|
||
`可达 · ${baseUrl} · whoami ok=${Boolean(data?.ok ?? true)}`,
|
||
"ok"
|
||
);
|
||
} catch (e) {
|
||
setStatus(formatFetchError(e, baseUrl || $("baseUrl").value), "err");
|
||
}
|
||
};
|
||
|
||
$("connect").onclick = async () => {
|
||
setStatus("登录中…");
|
||
setWorkspaceHint("", "");
|
||
const password = $("password").value;
|
||
let baseUrl = "";
|
||
try {
|
||
baseUrl = normalizeBaseUrl($("baseUrl").value);
|
||
// 关键:在点击手势内申请 host 权限,再交给 background 登录
|
||
const ok = await requestBaseUrlPermission(baseUrl);
|
||
if (!ok) {
|
||
setStatus(
|
||
`已取消访问权限:需要允许扩展访问 ${new URL(baseUrl).origin}(含端口)`,
|
||
"err"
|
||
);
|
||
return;
|
||
}
|
||
const res = await chrome.runtime.sendMessage({
|
||
type: "CONNECT",
|
||
baseUrl,
|
||
// 留空 → background 调 default workspace
|
||
rootUri: ($("rootUri").value || "").trim(),
|
||
email: $("email").value,
|
||
password,
|
||
});
|
||
$("password").value = "";
|
||
if (!res?.ok) {
|
||
setStatus(res?.error || "连接失败", "err");
|
||
return;
|
||
}
|
||
const r = res.result || {};
|
||
if (r.rootUri) $("rootUri").value = r.rootUri;
|
||
setStatus(
|
||
`已连接 · ${r.email || r.userId || ""} · 过期 ${r.expiresAt || "—"}`,
|
||
"ok"
|
||
);
|
||
setWorkspaceHint(r.rootUri, r.rootPath, r.workspaceSource || "default");
|
||
} catch (e) {
|
||
$("password").value = "";
|
||
setStatus(formatFetchError(e, baseUrl || $("baseUrl").value), "err");
|
||
}
|
||
};
|
||
|
||
$("disconnect").onclick = async () => {
|
||
await chrome.runtime.sendMessage({ type: "DISCONNECT" });
|
||
setStatus("已断开", "err");
|
||
setWorkspaceHint("", "");
|
||
};
|
||
|
||
hydrate();
|