- Service app/services/web_clipper.py: sanitize HTML, html->blocks, extraction article, creation page workspace-aware, rate limit 50/h, device registration - Router app/routers/web_clipper.py: POST /api/v2/web-clipper/clip, GET /status, POST /auth/verify, GET/DELETE /devices, GET /extensions (download page), auth via session ou Bearer (api_tokens / extension_devices) - Migration 19: extension_devices + extension_clips (+ indexes) - Extension Manifest V3: content.js (floating button, selection), background.js (clip + contextMenus), popup.html/js, clipper.css, icons - Settings UI: onglet Extensions (liste devices, revoke, test clip, liens download), page /extensions - Tests: 16 tests web_clipper (sanitize, blocks, article/bookmark/selection/screenshot, bearer, rate-limit, devices, extensions page) - Bump version 6.1.0 -> 6.2.0
121 lines
3.6 KiB
JavaScript
121 lines
3.6 KiB
JavaScript
// FlowDeck Web Clipper — background service worker (Manifest V3)
|
|
const STORAGE_KEY = "flowdeck_clipper";
|
|
const DEFAULT_SERVER = "http://localhost:8080";
|
|
|
|
async function getConfig() {
|
|
const data = await chrome.storage.local.get(STORAGE_KEY);
|
|
return data[STORAGE_KEY] || { serverUrl: DEFAULT_SERVER, token: "", deviceId: "" };
|
|
}
|
|
|
|
async function setConfig(cfg) {
|
|
await chrome.storage.local.set({ [STORAGE_KEY]: cfg });
|
|
}
|
|
|
|
function genDeviceId() {
|
|
return "dev_" + Math.random().toString(36).slice(2, 10) + Math.random().toString(36).slice(2, 10);
|
|
}
|
|
|
|
async function ensureDeviceId() {
|
|
const cfg = await getConfig();
|
|
if (!cfg.deviceId) {
|
|
cfg.deviceId = genDeviceId();
|
|
await setConfig(cfg);
|
|
}
|
|
return cfg.deviceId;
|
|
}
|
|
|
|
chrome.runtime.onInstalled.addListener(async () => {
|
|
await ensureDeviceId();
|
|
chrome.contextMenus.create({
|
|
id: "fd-clip-selection",
|
|
title: "📌 Clip selection to FlowDeck",
|
|
contexts: ["selection"]
|
|
});
|
|
chrome.contextMenus.create({
|
|
id: "fd-clip-page",
|
|
title: "📌 Clip page to FlowDeck",
|
|
contexts: ["page"]
|
|
});
|
|
});
|
|
|
|
chrome.contextMenus.onClicked.addListener(async (info, tab) => {
|
|
if (!tab || !tab.id) return;
|
|
if (info.menuItemId === "fd-clip-selection") {
|
|
chrome.tabs.sendMessage(tab.id, { action: "clipSelection" });
|
|
} else if (info.menuItemId === "fd-clip-page") {
|
|
chrome.scripting.executeScript({
|
|
target: { tabId: tab.id },
|
|
func: () => window.__fdOpenClipper && window.__fdOpenClipper("article")
|
|
});
|
|
}
|
|
});
|
|
|
|
// Unified clip handler
|
|
async function handleClip(data) {
|
|
const cfg = await getConfig();
|
|
const serverUrl = (cfg.serverUrl || DEFAULT_SERVER).replace(/\/$/, "");
|
|
const deviceId = cfg.deviceId || (await ensureDeviceId());
|
|
const token = cfg.token || "";
|
|
|
|
// Require token or let server handle session cookie fallback
|
|
if (!token) {
|
|
// Try clipboard without token; server may accept session if user logged in flowdeck tab
|
|
// but background fetch cannot send cookie cross-origin without credentials; warn
|
|
}
|
|
|
|
const payload = {
|
|
url: data.url,
|
|
title: data.title,
|
|
content: data.html || data.content,
|
|
content_type: data.content_type || "article",
|
|
selection_html: data.selection_html || "",
|
|
device_id: deviceId,
|
|
metadata: data.metadata || {}
|
|
};
|
|
|
|
const headers = { "Content-Type": "application/json", "X-Device-Id": deviceId };
|
|
if (token) headers["Authorization"] = "Bearer " + token;
|
|
|
|
const resp = await fetch(serverUrl + "/api/v2/web-clipper/clip", {
|
|
method: "POST",
|
|
headers,
|
|
body: JSON.stringify(payload)
|
|
});
|
|
const body = await resp.json().catch(() => ({}));
|
|
if (!resp.ok) {
|
|
return { error: body.detail || body.error || "clip_failed", status: resp.status, ...body };
|
|
}
|
|
return body;
|
|
}
|
|
|
|
async function handleAuth() {
|
|
// Token is set via popup; verify it
|
|
const cfg = await getConfig();
|
|
if (!cfg.token) return { error: "no_token" };
|
|
const serverUrl = (cfg.serverUrl || DEFAULT_SERVER).replace(/\/$/, "");
|
|
const resp = await fetch(serverUrl + "/api/v2/web-clipper/status", {
|
|
headers: { Authorization: "Bearer " + cfg.token }
|
|
});
|
|
const body = await resp.json().catch(() => ({}));
|
|
return body;
|
|
}
|
|
|
|
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
|
if (message.action === "clip") {
|
|
handleClip(message.data).then((r) => sendResponse(r));
|
|
return true;
|
|
}
|
|
if (message.action === "auth") {
|
|
handleAuth().then((r) => sendResponse(r));
|
|
return true;
|
|
}
|
|
if (message.action === "getConfig") {
|
|
getConfig().then((c) => sendResponse(c));
|
|
return true;
|
|
}
|
|
if (message.action === "setConfig") {
|
|
setConfig(message.config).then(() => sendResponse({ ok: true }));
|
|
return true;
|
|
}
|
|
});
|