feat: purge legacy agent hosts and land vault Chrome extension path

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.
This commit is contained in:
Agent Board
2026-07-25 14:25:37 +08:00
parent bc6f8488ee
commit 262e66b02e
137 changed files with 9018 additions and 46049 deletions
+291
View File
@@ -0,0 +1,291 @@
/**
* Persistent config (sync/local) vs session token.
*/
const SYNC_KEYS = [
"baseUrl",
"rootUri",
"rootPath",
"userId",
"email",
"connectedAt",
/** 上次保存选用的分组路径,扩展确认层默认选中 */
"lastFolderPath",
/**
* 去重记忆(不跨浏览器配置同步,仅 local):
* - savedSites: { [origin]: { [usernameKey]: { credentialId, passwordFp, savedAt, hasSession? } } }
* - dismissedSites: { [origin]: { [usernameKey]: dismissedAt }
* - neverAskOrigins: { [origin]: true }
*/
"savedSites",
"dismissedSites",
"neverAskOrigins",
];
const SESSION_KEYS = ["token", "tokenExpiresAt", "jti"];
/** Normalize username for dedupe keys. */
export function normalizeUsernameKey(username) {
return String(username || "")
.trim()
.toLowerCase();
}
/**
* Lightweight non-cryptographic fingerprint for password equality checks.
* Not a security hash — only used to avoid re-prompting the same login.
* @param {string} password
*/
export function passwordFingerprint(password) {
const s = String(password || "");
// FNV-1a 32-bit + length, enough for local dedupe.
let h = 0x811c9dc5;
for (let i = 0; i < s.length; i++) {
h ^= s.charCodeAt(i);
h = Math.imul(h, 0x01000193);
}
return `fp_${(h >>> 0).toString(16)}_${s.length}`;
}
/**
* @param {string} pageUrl
* @returns {string} origin or ""
*/
export function originFromUrl(pageUrl) {
try {
return new URL(String(pageUrl || "")).origin;
} catch {
return "";
}
}
/**
* @returns {Promise<{savedSites:object, dismissedSites:object, neverAskOrigins:object}>}
*/
export async function loadDedupeState() {
const data = await chrome.storage.local.get([
"savedSites",
"dismissedSites",
"neverAskOrigins",
]);
return {
savedSites:
data.savedSites && typeof data.savedSites === "object"
? data.savedSites
: {},
dismissedSites:
data.dismissedSites && typeof data.dismissedSites === "object"
? data.dismissedSites
: {},
neverAskOrigins:
data.neverAskOrigins && typeof data.neverAskOrigins === "object"
? data.neverAskOrigins
: {},
};
}
/**
* Remember a successful save so the same site+user+password does not re-prompt.
* @param {{ pageUrl: string, username?: string, password?: string, credentialId?: string, hasSession?: boolean }} info
*/
export async function rememberSavedSite(info) {
const origin = originFromUrl(info.pageUrl);
if (!origin) return;
const userKey = normalizeUsernameKey(info.username) || "_";
const state = await loadDedupeState();
const byOrigin = { ...(state.savedSites[origin] || {}) };
byOrigin[userKey] = {
credentialId: info.credentialId || byOrigin[userKey]?.credentialId || "",
passwordFp: passwordFingerprint(info.password || ""),
savedAt: new Date().toISOString(),
hasSession: Boolean(info.hasSession),
};
const savedSites = { ...state.savedSites, [origin]: byOrigin };
// Clear dismiss for this user after explicit save.
const dismissed = { ...(state.dismissedSites[origin] || {}) };
delete dismissed[userKey];
const dismissedSites = { ...state.dismissedSites };
if (Object.keys(dismissed).length) dismissedSites[origin] = dismissed;
else delete dismissedSites[origin];
await chrome.storage.local.set({ savedSites, dismissedSites });
}
/**
* User cancelled save prompt for this site+username (suppress for a while).
* @param {{ pageUrl: string, username?: string, forever?: boolean }} info
*/
export async function rememberDismissedSite(info) {
const origin = originFromUrl(info.pageUrl);
if (!origin) return;
if (info.forever) {
const state = await loadDedupeState();
await chrome.storage.local.set({
neverAskOrigins: { ...state.neverAskOrigins, [origin]: true },
});
return;
}
const userKey = normalizeUsernameKey(info.username) || "_";
const state = await loadDedupeState();
const byOrigin = { ...(state.dismissedSites[origin] || {}) };
byOrigin[userKey] = Date.now();
await chrome.storage.local.set({
dismissedSites: { ...state.dismissedSites, [origin]: byOrigin },
});
}
/**
* Decide whether auto-prompt should be suppressed for this login draft.
* @param {{ pageUrl: string, username?: string, password?: string }} draft
* @param {{ dismissTtlMs?: number }} [opts]
* @returns {Promise<{ suppress: boolean, reason?: string, credentialId?: string, needSessionOnly?: boolean }>}
*/
export async function shouldSuppressPrompt(draft, opts = {}) {
const origin = originFromUrl(draft.pageUrl);
if (!origin) return { suppress: false };
const userKey = normalizeUsernameKey(draft.username) || "_";
const state = await loadDedupeState();
if (state.neverAskOrigins[origin]) {
return { suppress: true, reason: "never_ask_origin" };
}
const dismissTtlMs = opts.dismissTtlMs ?? 14 * 24 * 60 * 60 * 1000;
const dismissedAt = state.dismissedSites?.[origin]?.[userKey];
if (typeof dismissedAt === "number" && Date.now() - dismissedAt < dismissTtlMs) {
return { suppress: true, reason: "recently_dismissed" };
}
const saved = state.savedSites?.[origin]?.[userKey];
if (saved && draft.password) {
const fp = passwordFingerprint(draft.password);
if (saved.passwordFp === fp) {
// Same account+password already saved: do not re-ask for credential.
// If session was never captured, allow a silent session-only update path.
return {
suppress: true,
reason: "already_saved_same_password",
credentialId: saved.credentialId || "",
needSessionOnly: !saved.hasSession,
};
}
}
return { suppress: false };
}
/**
* @returns {Promise<{baseUrl?:string, rootUri?:string, userId?:string, email?:string, connectedAt?:string, lastFolderPath?:string}>}
*/
export async function loadPersistent() {
return chrome.storage.local.get(SYNC_KEYS);
}
/**
* @param {object} data
*/
export async function savePersistent(data) {
/** @type {Record<string, unknown>} */
const patch = {};
for (const k of SYNC_KEYS) {
if (data[k] !== undefined) patch[k] = data[k];
}
await chrome.storage.local.set(patch);
}
/**
* @returns {Promise<{token?:string, tokenExpiresAt?:string, jti?:string}>}
*/
export async function loadSession() {
// Prefer session storage (cleared on browser restart) when available.
if (chrome.storage.session) {
return chrome.storage.session.get(SESSION_KEYS);
}
return chrome.storage.local.get(SESSION_KEYS);
}
/**
* @param {object} data
*/
export async function saveSession(data) {
/** @type {Record<string, unknown>} */
const patch = {};
for (const k of SESSION_KEYS) {
if (data[k] !== undefined) patch[k] = data[k];
}
if (chrome.storage.session) {
await chrome.storage.session.set(patch);
} else {
await chrome.storage.local.set(patch);
}
}
export async function clearSession() {
if (chrome.storage.session) {
await chrome.storage.session.remove(SESSION_KEYS);
}
await chrome.storage.local.remove(SESSION_KEYS);
}
/**
* Full runtime config for API calls.
* @returns {Promise<{baseUrl:string, rootUri:string, token?:string, userId?:string, email?:string, connected:boolean}>}
*/
export async function loadConfig() {
const [p, s] = await Promise.all([loadPersistent(), loadSession()]);
const baseUrl = (p.baseUrl || "").trim();
const rootUri = (p.rootUri || "").trim();
const token = (s.token || "").trim();
const connected = Boolean(baseUrl && rootUri && token);
return {
baseUrl,
rootUri,
token: token || undefined,
userId: p.userId,
email: p.email,
connected,
tokenExpiresAt: s.tokenExpiresAt,
jti: s.jti,
lastFolderPath: p.lastFolderPath || "",
};
}
/**
* 规范化逻辑分组路径(与 vault store normalize_folder_path 对齐)。
* @param {string} [raw]
* @returns {string}
*/
export function normalizeFolderPath(raw) {
const s = String(raw || "").trim();
if (!s) return "";
const parts = s
.split(/[/\\]+/)
.map((p) => p.trim())
.filter((p) => p && p !== "." && p !== "..");
return parts.join("/");
}
/**
* 记住用户上次选用的分组。
* @param {string} folderPath
*/
export async function saveLastFolderPath(folderPath) {
const fp = normalizeFolderPath(folderPath);
await savePersistent({ lastFolderPath: fp });
}
/**
* @param {boolean} connected
* @param {string} [label]
*/
export async function setBadge(connected, label) {
try {
if (connected) {
await chrome.action.setBadgeText({ text: "ON" });
await chrome.action.setBadgeBackgroundColor({ color: "#0a7" });
if (label) {
await chrome.action.setTitle({ title: `MNote Vault · ${label}` });
}
} else {
await chrome.action.setBadgeText({ text: "—" });
await chrome.action.setBadgeBackgroundColor({ color: "#888" });
await chrome.action.setTitle({ title: "MNote Vault · 未连接" });
}
} catch {
// ignore in non-extension env
}
}