chrome.runtime.onInstalled.addListener(async (details) => {
console.log('MiniMark 已安装/更新');
const config = await chrome.storage.local.get(['autoSync', 'syncInterval']);
if (config.syncInterval === undefined) {
await chrome.storage.local.set({
autoSync: false,
syncInterval: 1440,
lastSyncTime: null,
syncStatus: null
});
}
if (config.autoSync) {
setupAutoSync(config.syncInterval || 1440);
}
if (details.reason === 'install') {
try { await markUnsynced(); } catch(e) { console.warn('初始化标记未同步失败:', e); }
}
});
chrome.bookmarks.onCreated.addListener(() => { markUnsynced().catch(e=>console.warn(e)); });
chrome.bookmarks.onRemoved.addListener(() => { markUnsynced().catch(e=>console.warn(e)); });
chrome.bookmarks.onChanged.addListener(() => { markUnsynced().catch(e=>console.warn(e)); });
chrome.bookmarks.onMoved.addListener(() => { markUnsynced().catch(e=>console.warn(e)); });
chrome.runtime.onStartup.addListener(() => {
updateActionBadge().catch(e=>console.warn(e));
cleanupOnStartup().catch(e=>console.warn(e));
});
async function markUnsynced() {
try {
const res = await chrome.storage.local.get(['lastSyncCount']);
const tree = await chrome.bookmarks.getTree();
const current = countBookmarks(tree[0]);
const last = res.lastSyncCount !== undefined ? res.lastSyncCount : null;
const hasUnsynced = (last === null && current > 0) || (last !== null && current !== last);
await chrome.storage.local.set({ hasUnsynced });
await updateActionBadge();
} catch (e) {
console.warn('标记未同步失败:', e);
}
}
async function updateActionBadge() {
try {
const res = await chrome.storage.local.get(['hasUnsynced', 'githubToken', 'giteeToken']);
const has = !!res.hasUnsynced;
const loggedIn = !!(res.githubToken || res.giteeToken);
if (!loggedIn) {
chrome.action.setBadgeText({ text: '' });
return;
}
if (has) {
chrome.action.setBadgeText({ text: '●' });
try { chrome.action.setBadgeBackgroundColor({ color: '#FF3B30' }); } catch(e) { }
} else {
chrome.action.setBadgeText({ text: '' });
}
} catch (e) {
console.warn('更新角标失败:', e);
}
}
chrome.storage.onChanged.addListener((changes, areaName) => {
if (areaName !== 'local') return;
const keys = Object.keys(changes);
const watched = ['hasUnsynced', 'githubToken', 'giteeToken', 'lastSyncCount'];
if (keys.some(k => watched.includes(k))) {
updateActionBadge().catch(e => console.warn('storage change 更新角标失败:', e));
}
});
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
if (request.action === 'syncNow' || request.action === 'uploadToRemote') {
performSync(request.platform)
.then(result => sendResponse({ success: true, gistId: result.gistId, count: result.count }))
.catch(error => sendResponse({ success: false, error: error.message }));
return true;
} else if (request.action === 'downloadFromRemote') {
restoreFromRemote(request.platform)
.then(result => sendResponse({ success: true, count: result.count }))
.catch(error => sendResponse({ success: false, error: error.message }));
return true;
} else if (request.action === 'getRemoteBookmarkCount') {
getRemoteBookmarkCount(request.platform)
.then(count => sendResponse({ success: true, count }))
.catch(error => sendResponse({ success: false, error: error.message }));
return true;
} else if (request.action === 'verifyToken') {
verifyToken(request.platform)
.then((user) => sendResponse({ success: true, user }))
.catch((error) => sendResponse({ success: false, error: error.message }));
return true;
} else if (request.action === 'findExistingGist') {
findExistingGist(request.platform)
.then(result => sendResponse({ success: true, gistId: result.gistId, count: result.count }))
.catch(error => sendResponse({ success: false, error: error.message }));
return true;
} else if (request.action === 'analyzeSyncStatus') {
analyzeSyncStatus(request.platform)
.then(analysis => sendResponse({ success: true, analysis }))
.catch(error => sendResponse({ success: false, error: error.message || String(error) }));
return true;
} else if (request.action === 'cleanupOldGists') {
const platforms = request.platform === 'all'
? ['github', 'gitee']
: [request.platform || 'all'];
const tasks = [];
if (platforms.includes('github') || platforms.includes('all')) tasks.push(cleanupOldGists('github'));
if (platforms.includes('gitee') || platforms.includes('all')) tasks.push(cleanupOldGists('gitee'));
Promise.all(tasks)
.then(() => sendResponse({ success: true }))
.catch(error => sendResponse({ success: false, error: error.message }));
return true;
} else if (request.action === 'updateAutoSync') {
if (request.enabled) {
chrome.storage.local.get(['syncInterval'], (result) => {
setupAutoSync(result.syncInterval || 1440);
});
} else {
chrome.alarms.clear('autoSync');
}
sendResponse({ success: true });
} else if (request.action === 'clearLocalBookmarks' || request.action === 'clearAllBookmarks') {
clearLocalBookmarks()
.then(() => sendResponse({ success: true }))
.catch(error => sendResponse({ success: false, error: error.message }));
return true;
}
});
chrome.alarms.onAlarm.addListener((alarm) => {
if (alarm.name === 'autoSync') {
console.log('执行自动同步...');
performAutoSync().catch(error => {
console.error('自动同步失败:', error);
});
}
});
function setupAutoSync(intervalMinutes) {
chrome.alarms.create('autoSync', {
periodInMinutes: intervalMinutes
});
console.log(`自动同步已设置,间隔: ${intervalMinutes} 分钟`);
}
async function performAutoSync() {
try {
const config = await chrome.storage.local.get([
'githubToken',
'giteeToken',
'githubGistId',
'giteeGistId',
'syncMode'
]);
const bookmarks = await chrome.bookmarks.getTree();
const localCount = countBookmarks(bookmarks[0]);
const platforms = [];
if (config.githubToken) platforms.push('github');
if (config.giteeToken) platforms.push('gitee');
for (const platform of platforms) {
try {
const gistId = platform === 'github' ? config.githubGistId : config.giteeGistId;
const syncMode = config.syncMode;
if (syncMode === 'merge') {
console.log(`${platform}: 合并模式 - 双向合并`);
await performSync(platform);
continue;
}
if (!gistId) {
console.log(`${platform}: 没有远程备份,执行上传`);
await performSync(platform);
continue;
}
const remoteCount = await getRemoteBookmarkCount(platform);
console.log(`${platform}: 本地 ${localCount} 个,远程 ${remoteCount} 个`);
if (remoteCount > localCount) {
console.log(`${platform}: 远程书签更多,从远程同步到本地`);
await restoreFromRemote(platform);
} else {
console.log(`${platform}: 本地书签更多或相等,上传到远程`);
await performSync(platform);
}
} catch (error) {
console.error(`${platform} 自动同步失败:`, error);
}
}
console.log('自动同步完成');
} catch (error) {
console.error('自动同步失败:', error);
throw error;
}
}
async function performSync(platform) {
try {
const config = await chrome.storage.local.get([
'githubToken',
'giteeToken',
'githubGistId',
'giteeGistId'
]);
if (!platform) {
throw new Error('未指定同步平台');
}
const token = platform === 'github' ? config.githubToken : config.giteeToken;
const gistId = platform === 'github' ? config.githubGistId : config.giteeGistId;
if (!token) {
throw new Error(`未配置 ${platform === 'github' ? 'GitHub' : 'Gitee'} Token`);
}
if (!gistId) {
try {
const existing = await findExistingGist(platform);
if (existing && existing.gistId) {
gistId = existing.gistId;
if (platform === 'github') config.githubGistId = gistId;
else config.giteeGistId = gistId;
console.log(`${platform}: 复用已存在的 Gist ${gistId},不再新建`);
}
} catch (e) {
console.warn(`查找已存在 ${platform} Gist 失败,将继续创建新 Gist:`, e);
}
}
if (!gistId) {
try {
const existing = await findExistingGist(platform);
if (existing && existing.gistId) {
gistId = existing.gistId;
if (platform === 'github') config.githubGistId = gistId;
else config.giteeGistId = gistId;
console.log(`${platform}: 复用已存在的 Gist ${gistId},不再新建`);
}
} catch (e) {
console.warn(`查找已存在 ${platform} Gist 失败,将继续创建新 Gist:`, e);
}
}
const syncMode = (await chrome.storage.local.get(['syncMode'])).syncMode;
if (syncMode === 'merge') {
try {
const remote = await fetchRemoteBookmarkData(platform);
if (remote && remote.bookmarks) {
await mergeRemoteIntoLocal(remote.bookmarks);
console.log('合并模式:已把远端书签合并进本地');
}
} catch (e) {
console.warn('合并远端到本地失败(继续以本地为准上传):', e);
}
}
const bookmarks = await chrome.bookmarks.getTree();
const bookmarkCount = countBookmarks(bookmarks[0]);
const cleanedBookmarks = bookmarks;
const bookmarkData = {
version: '1.0',
timestamp: Date.now(),
count: bookmarkCount,
bookmarks: cleanedBookmarks
};
const dataSize = JSON.stringify(bookmarkData).length;
const dataSizeMB = (dataSize / 1024 / 1024).toFixed(2);
console.log(`书签数据大小: ${dataSizeMB} MB (${dataSize} 字节)`);
if (platform === 'gitee' && dataSize > 1024 * 1024) {
throw new Error(`书签数据过大 (${dataSizeMB} MB),Gitee 限制单个文件最大 1MB。请考虑使用 GitHub 或减少书签数量。`);
}
let result;
if (platform === 'github') {
result = await syncToGithub(token, gistId, bookmarkData);
} else if (platform === 'gitee') {
result = await syncToGitee(token, gistId, bookmarkData);
} else {
throw new Error('不支持的同步平台');
}
const now = Date.now();
const storageData = {
lastSyncTime: now,
lastSyncCount: bookmarkCount,
syncStatus: 'success'
};
if (platform === 'github') {
storageData.githubGistId = result.gistId;
storageData.githubRemoteCount = bookmarkCount;
storageData.githubSyncStatus = 'success';
storageData.githubLastSyncTime = now;
storageData.githubLastSyncCount = bookmarkCount;
} else {
storageData.giteeGistId = result.gistId;
storageData.giteeRemoteCount = bookmarkCount;
storageData.giteeSyncStatus = 'success';
storageData.giteeLastSyncTime = now;
storageData.giteeLastSyncCount = bookmarkCount;
}
await chrome.storage.local.set(storageData);
try {
await chrome.storage.local.set({ hasUnsynced: false });
await updateActionBadge();
} catch (e) {
console.warn('同步成功后清除未同步标记失败:', e);
}
try {
const history = await chrome.storage.local.get(['syncHistory']);
const list = history.syncHistory || [];
list.unshift({ platform, action: 'upload', status: 'success', time: now, count: bookmarkCount });
await chrome.storage.local.set({ syncHistory: list.slice(0, 50) });
} catch (e) {
console.warn('记录同步历史失败:', e);
}
try {
chrome.runtime.sendMessage({ action: 'platformSyncUpdate', platform, status: 'success', time: now, count: bookmarkCount, type: 'upload' }).catch(() => {});
} catch (e) { }
console.log('同步成功:', result);
return { gistId: result.gistId, url: result.url, updatedAt: result.updatedAt, count: bookmarkCount };
} catch (error) {
console.error('同步失败:', error);
try {
const errData = { syncStatus: 'error' };
const nowErr = Date.now();
if (platform === 'github') {
errData.githubSyncStatus = 'error';
errData.githubLastSyncTime = nowErr;
} else if (platform === 'gitee') {
errData.giteeSyncStatus = 'error';
errData.giteeLastSyncTime = nowErr;
}
await chrome.storage.local.set(errData);
try {
const history = await chrome.storage.local.get(['syncHistory']);
const list = history.syncHistory || [];
list.unshift({ platform, action: 'upload', status: 'error', time: nowErr, count: bookmarkCount });
await chrome.storage.local.set({ syncHistory: list.slice(0, 50) });
} catch (e) {
console.warn('记录失败同步历史失败:', e);
}
try {
chrome.runtime.sendMessage({ action: 'platformSyncUpdate', platform, status: 'error', time: nowErr, count: bookmarkCount, type: 'upload' }).catch(() => {});
} catch (e) { }
} catch (e) {
console.warn('记录后台同步错误状态失败:', e);
}
throw error;
}
}
async function verifyToken(platform) {
const cfg = await chrome.storage.local.get(['githubToken', 'giteeToken']);
const token = platform === 'github' ? cfg.githubToken : platform === 'gitee' ? cfg.giteeToken : null;
if (!token) throw new Error('未配置 Token');
if (platform === 'github') return await verifyGithubToken(token);
if (platform === 'gitee') return await verifyGiteeToken(token);
throw new Error('不支持的平台');
}
async function verifyGithubToken(token) {
const r = await fetch('https://api.github.com/user', {
headers: { 'Authorization': 'Bearer ' + token, 'Accept': 'application/vnd.github.v3+json' }
});
if (!r.ok) {
const t = await r.text().catch(() => '');
throw new Error('GitHub 验证失败: ' + r.status + (t ? ' ' + t.slice(0, 120) : ''));
}
const u = await r.json();
return {
name: u.name || u.login || 'GitHub 用户',
login: u.login || '',
avatarUrl: u.avatar_url || ''
};
}
async function verifyGiteeToken(token) {
const r = await fetch('https://gitee.com/api/v5/user?access_token=' + encodeURIComponent(token));
if (!r.ok) {
const t = await r.text().catch(() => '');
throw new Error('Gitee 验证失败: ' + r.status + (t ? ' ' + t.slice(0, 120) : ''));
}
const u = await r.json();
return {
name: u.name || u.login || 'Gitee 用户',
login: u.login || '',
avatarUrl: u.avatar_url || ''
};
}
async function syncToGithub(token, gistId, bookmarkData) {
const url = gistId
? `https://api.github.com/gists/${gistId}`
: 'https://api.github.com/gists';
const method = gistId ? 'PATCH' : 'POST';
const body = {
description: 'MiniBookmark',
public: false,
files: {
'bookmarks.json': {
content: JSON.stringify(bookmarkData, null, 2)
}
}
};
const response = await fetch(url, {
method: method,
headers: {
'Authorization': `Bearer ${token}`,
'Accept': 'application/vnd.github.v3+json',
'Content-Type': 'application/json'
},
body: JSON.stringify(body)
});
if (!response.ok) {
const errorText = await response.text();
let errorMsg;
try {
const error = JSON.parse(errorText);
errorMsg = error.message || error.error || `GitHub API 错误: ${response.status}`;
} catch (e) {
errorMsg = `GitHub API 错误: ${response.status} - ${errorText}`;
}
console.error('GitHub API 错误详情:', errorMsg);
throw new Error(errorMsg);
}
const result = await response.json();
return {
gistId: result.id,
url: result.html_url,
updatedAt: result.updated_at
};
}
function sanitizeForGitee(text) {
if (typeof text !== 'string') return text;
return text.replace(/[\u{10000}-\u{10FFFF}]/gu, '');
}
async function syncToGitee(token, gistId, bookmarkData) {
console.log('开始同步到 Gitee...');
const safeContent = sanitizeForGitee(JSON.stringify(bookmarkData));
const auth = `?access_token=${encodeURIComponent(token)}`;
const headers = { 'Content-Type': 'application/json' };
const patchBody = JSON.stringify({
files: { 'bookmarks.json': { content: safeContent, filename: 'bookmarks.json' } }
});
const patchGist = async (id) => {
const r = await fetch(`https://gitee.com/api/v5/gists/${id}${auth}`, {
method: 'PATCH', headers, body: patchBody
});
if (r.ok) {
const res = await r.json();
return res;
}
console.warn('Gitee 更新失败(尝试下一候选):', r.status, await r.text().catch(() => ''));
return null;
};
const targets = [];
if (gistId) targets.push(gistId);
try {
const list = await fetchGiteeGists(token);
const candidates = (list || [])
.filter(g => g.files && g.files['bookmarks.json'])
.sort((a, b) =>
new Date(b.updated_at || b.created_at || 0).getTime() -
new Date(a.updated_at || a.created_at || 0).getTime()
);
if (candidates[0]) targets.push(candidates[0].id);
} catch (e) {
console.warn('获取 Gitee 片段列表失败(将仅尝试记录的 gistId):', e);
}
const tried = new Set();
for (const id of targets) {
if (!id || tried.has(id)) continue;
tried.add(id);
try {
const res = await patchGist(id);
if (res) {
console.log('Gitee 更新成功,Gist ID:', res.id);
return { gistId: res.id, url: res.html_url, updatedAt: res.updated_at };
}
} catch (e) {
console.warn('Gitee 更新异常(尝试下一候选):', e);
}
}
console.log('未找到可更新片段,新建 Gist...');
const createResponse = await fetch(`https://gitee.com/api/v5/gists${auth}`, {
method: 'POST',
headers,
body: JSON.stringify({
files: {
'bookmarks.json': {
content: safeContent
}
},
description: 'MiniBookmark',
public: false
})
});
if (!createResponse.ok) {
const errorText = await createResponse.text();
console.error('创建 Gist 失败:', errorText);
throw new Error(`Gitee 创建 Gist 失败: ${createResponse.status} ${errorText}`);
}
const createResult = await createResponse.json();
if (!createResult || !createResult.id) {
throw new Error('Gitee 创建 Gist 失败:响应中缺少 Gist ID');
}
console.log('Gitee 新建成功,Gist ID:', createResult.id);
return {
gistId: createResult.id,
url: createResult.html_url,
updatedAt: createResult.updated_at
};
}
async function mergeIntoLocal(remoteNodes, parentId) {
if (!Array.isArray(remoteNodes) || !parentId) return;
let existing = [];
try { existing = await chrome.bookmarks.getChildren(parentId); } catch (e) { return; }
const existingByKey = new Map();
for (const n of existing) {
const key = n.url ? ('u:' + n.url) : ('f:' + (n.title || ''));
if (!existingByKey.has(key)) existingByKey.set(key, n);
}
for (const r of remoteNodes) {
const key = r.url ? ('u:' + r.url) : ('f:' + (r.title || ''));
const found = existingByKey.get(key);
if (found) {
if (!r.url && r.children && found.url === undefined) {
await mergeIntoLocal(r.children, found.id);
}
continue;
}
try {
if (r.url) {
await chrome.bookmarks.create({ parentId, title: r.title || '', url: r.url });
} else {
const folder = await chrome.bookmarks.create({ parentId, title: r.title || '文件夹' });
if (r.children) await mergeIntoLocal(r.children, folder.id);
}
} catch (e) {
console.warn('合并创建书签失败:', e);
}
}
}
async function mergeRemoteIntoLocal(remoteBookmarks) {
if (!remoteBookmarks || !remoteBookmarks[0]) return;
const localTree = await chrome.bookmarks.getTree();
const localRoot = localTree[0];
for (const remoteTop of (remoteBookmarks[0].children || [])) {
let localTop = (localRoot.children || []).find(c => !c.url && c.title === remoteTop.title);
if (!localTop) {
try {
localTop = await chrome.bookmarks.create({ parentId: localRoot.id, title: remoteTop.title || '文件夹' });
} catch (e) { console.warn('创建顶层文件夹失败:', e); continue; }
}
await mergeIntoLocal(remoteTop.children || [], localTop.id);
}
}
async function fetchRemoteBookmarkData(platform) {
const config = await chrome.storage.local.get([
'githubToken', 'giteeToken', 'githubGistId', 'giteeGistId'
]);
const token = platform === 'github' ? config.githubToken : config.giteeToken;
const gistId = platform === 'github' ? config.githubGistId : config.giteeGistId;
if (!token || !gistId) return null;
try {
return platform === 'github' ? await fetchFromGithub(token, gistId) : await fetchFromGitee(token, gistId);
} catch (e) {
console.warn('获取远端书签数据失败:', e);
return null;
}
}
function flattenBookmarks(treeNode, folderPath = '', orderPrefix = '', index = 0, out = []) {
const children = Array.isArray(treeNode) ? treeNode : (treeNode && treeNode.children) || [];
children.forEach((node, i) => {
const title = node.title || '';
const path = folderPath ? `${folderPath} / ${title}` : title;
const orderKey = `${orderPrefix}${i}.${node.url ? node.url : title}`;
if (node.url) {
out.push({ title, url: node.url, folderPath: path, orderKey });
}
if (node.children) {
flattenBookmarks(node.children, path, orderKey + '/', 0, out);
}
});
return out;
}
async function analyzeSyncStatus(platform) {
const localTree = await chrome.bookmarks.getTree();
const localFlat = flattenBookmarks(localTree);
let remoteFlat = [];
let remoteError = null;
try {
const remoteData = await fetchRemoteBookmarkData(platform);
if (remoteData && remoteData.bookmarks) {
remoteFlat = flattenBookmarks(remoteData.bookmarks);
}
} catch (e) {
remoteError = e.message || String(e);
}
const localUrls = new Set(localFlat.filter(b => b.url).map(b => b.url));
const remoteUrls = new Set(remoteFlat.filter(b => b.url).map(b => b.url));
const localOnlyItems = localFlat.filter(b => b.url && !remoteUrls.has(b.url));
const remoteOnlyItems = remoteFlat.filter(b => b.url && !localUrls.has(b.url));
const bothCount = localFlat.filter(b => b.url && remoteUrls.has(b.url)).length;
const remoteByUrl = new Map();
for (const b of remoteFlat) {
if (!remoteByUrl.has(b.url)) remoteByUrl.set(b.url, b.orderKey);
}
const orderChangedItems = localFlat.filter(b => {
if (!b.url || !remoteUrls.has(b.url)) return false;
return remoteByUrl.get(b.url) !== b.orderKey;
}).map(b => {
const remote = remoteFlat.find(r => r.url === b.url);
return { ...b, remoteFolderPath: remote ? remote.folderPath : '' };
});
const orderChangedCount = orderChangedItems.length;
const SAMPLE = 50;
return {
platform,
localCount: localFlat.length,
remoteCount: remoteFlat.length,
localOnlyCount: localOnlyItems.length,
remoteOnlyCount: remoteOnlyItems.length,
bothCount,
orderChangedCount,
remoteError,
localOnlyItems: localOnlyItems.slice(0, SAMPLE),
remoteOnlyItems: remoteOnlyItems.slice(0, SAMPLE),
orderChangedItems: orderChangedItems.slice(0, SAMPLE),
localOnlyTotal: localOnlyItems.length,
remoteOnlyTotal: remoteOnlyItems.length,
orderChangedTotal: orderChangedItems.length,
hasMoreLocal: localOnlyItems.length > SAMPLE,
hasMoreRemote: remoteOnlyItems.length > SAMPLE,
hasMoreOrder: orderChangedItems.length > SAMPLE,
};
}
async function restoreFromRemote(platform) {
try {
const config = await chrome.storage.local.get([
'githubToken',
'giteeToken',
'githubGistId',
'giteeGistId'
]);
const token = platform === 'github' ? config.githubToken : config.giteeToken;
const gistId = platform === 'github' ? config.githubGistId : config.giteeGistId;
if (!gistId) {
throw new Error('没有找到远端备份');
}
let bookmarkData;
if (platform === 'github') {
bookmarkData = await fetchFromGithub(token, gistId);
} else if (platform === 'gitee') {
bookmarkData = await fetchFromGitee(token, gistId);
}
console.log('获取到远端书签数据:', bookmarkData);
console.log('书签结构:', JSON.stringify(bookmarkData.bookmarks, null, 2).substring(0, 500));
const syncMode = (await chrome.storage.local.get(['syncMode'])).syncMode;
if (syncMode === 'merge') {
if (bookmarkData && bookmarkData.bookmarks) {
await mergeRemoteIntoLocal(bookmarkData.bookmarks);
const mergedCount = bookmarkData.count || 0;
console.log('合并模式:已把远端书签合并进本地,完成恢复');
try {
const now = Date.now();
const data = {
lastSyncTime: now,
lastSyncCount: mergedCount,
syncStatus: 'success'
};
if (platform === 'github') {
data.githubSyncStatus = 'success';
data.githubLastSyncTime = now;
data.githubLastSyncCount = mergedCount;
} else if (platform === 'gitee') {
data.giteeSyncStatus = 'success';
data.giteeLastSyncTime = now;
data.giteeLastSyncCount = mergedCount;
}
await chrome.storage.local.set(data);
try {
const history = await chrome.storage.local.get(['syncHistory']);
const list = history.syncHistory || [];
list.unshift({ platform, action: 'download', status: 'success', time: now, count: mergedCount });
await chrome.storage.local.set({ syncHistory: list.slice(0, 50) });
} catch (e) { }
try {
chrome.runtime.sendMessage({ action: 'platformSyncUpdate', platform, status: 'success', time: now, count: mergedCount, type: 'download' }).catch(() => {});
} catch (e) { }
} catch (e) { }
return { count: mergedCount };
}
}
const tree = await chrome.bookmarks.getTree();
const root = tree[0];
console.log('开始清空本地书签...');
if (root.children) {
for (const folder of root.children) {
if (folder.children) {
for (const child of folder.children) {
try {
if (child.children) {
await chrome.bookmarks.removeTree(child.id);
} else {
await chrome.bookmarks.remove(child.id);
}
} catch (e) {
console.warn('删除书签失败:', e);
}
}
}
}
}
console.log('本地书签已清空');
if (bookmarkData.bookmarks && bookmarkData.bookmarks[0]) {
console.log('开始恢复书签...');
const currentTree = await chrome.bookmarks.getTree();
const currentRoot = currentTree[0];
console.log('当前书签根节点:', currentRoot);
console.log('远程书签根节点:', bookmarkData.bookmarks[0]);
if (currentRoot.children && bookmarkData.bookmarks[0].children) {
const remoteFolders = bookmarkData.bookmarks[0].children;
for (let i = 0; i < remoteFolders.length; i++) {
const remoteFolder = remoteFolders[i];
console.log('处理远程文件夹:', remoteFolder.title);
let localFolder = currentRoot.children.find(f => f.title === remoteFolder.title);
if (!localFolder && currentRoot.children[i]) {
localFolder = currentRoot.children[i];
}
if (localFolder && remoteFolder.children) {
console.log('找到本地文件夹:', localFolder.title, 'ID:', localFolder.id);
console.log('远程文件夹有', remoteFolder.children.length, '个子项');
await restoreBookmarkTree(remoteFolder, localFolder.id);
} else {
console.warn('未找到本地文件夹:', remoteFolder.title);
}
}
}
console.log('书签恢复完成');
} else {
console.error('远程书签数据格式错误');
}
try {
const now = Date.now();
const data = {
lastSyncTime: now,
lastSyncCount: bookmarkData.count || 0,
syncStatus: 'success'
};
if (platform === 'github') {
data.githubSyncStatus = 'success';
data.githubLastSyncTime = now;
data.githubLastSyncCount = bookmarkData.count || 0;
} else if (platform === 'gitee') {
data.giteeSyncStatus = 'success';
data.giteeLastSyncTime = now;
data.giteeLastSyncCount = bookmarkData.count || 0;
}
await chrome.storage.local.set(data);
try {
const history = await chrome.storage.local.get(['syncHistory']);
const list = history.syncHistory || [];
list.unshift({ platform, action: 'download', status: 'success', time: now, count: bookmarkData.count || 0 });
await chrome.storage.local.set({ syncHistory: list.slice(0, 50) });
} catch (e) {
console.warn('记录恢复历史失败:', e);
}
try {
chrome.runtime.sendMessage({ action: 'platformSyncUpdate', platform, status: 'success', time: now, count: bookmarkData.count || 0, type: 'download' }).catch(() => {});
} catch (e) { }
} catch (e) {
console.warn('保存恢复状态失败:', e);
}
return { count: bookmarkData.count || 0 };
} catch (error) {
console.error('恢复书签失败:', error);
try {
const nowErr = Date.now();
const errData = { syncStatus: 'error' };
if (platform === 'github') {
errData.githubSyncStatus = 'error';
errData.githubLastSyncTime = nowErr;
} else if (platform === 'gitee') {
errData.giteeSyncStatus = 'error';
errData.giteeLastSyncTime = nowErr;
}
await chrome.storage.local.set(errData);
try {
const history = await chrome.storage.local.get(['syncHistory']);
const list = history.syncHistory || [];
list.unshift({ platform, action: 'download', status: 'error', time: nowErr, count: bookmarkData && bookmarkData.count ? bookmarkData.count : 0 });
await chrome.storage.local.set({ syncHistory: list.slice(0, 50) });
} catch (e) {
console.warn('记录恢复失败历史失败:', e);
}
try {
chrome.runtime.sendMessage({ action: 'platformSyncUpdate', platform, status: 'error', time: nowErr, count: bookmarkData && bookmarkData.count ? bookmarkData.count : 0, type: 'download' }).catch(() => {});
} catch (e) { }
} catch (e) {
console.warn('记录后台恢复错误状态失败:', e);
}
throw error;
}
}
async function clearLocalBookmarks() {
const tree = await chrome.bookmarks.getTree();
const root = tree[0];
if (!root || !root.children) return;
for (const folder of root.children) {
if (!folder.children) continue;
for (const child of folder.children) {
try {
if (child.children) {
await chrome.bookmarks.removeTree(child.id);
} else {
await chrome.bookmarks.remove(child.id);
}
} catch (e) {
console.warn('删除书签失败:', e);
}
}
}
}
async function restoreBookmarkTree(node, parentId) {
if (!node.children) {
return;
}
for (const child of node.children) {
try {
if (child.url) {
await chrome.bookmarks.create({
parentId: parentId,
title: child.title,
url: child.url
});
} else if (child.children) {
const folder = await chrome.bookmarks.create({
parentId: parentId,
title: child.title
});
if (folder && folder.id) {
await restoreBookmarkTree(child, folder.id);
}
}
} catch (e) {
console.warn('恢复书签项失败:', e, child);
}
}
}
async function fetchFromGithub(token, gistId) {
const response = await fetch(`https://api.github.com/gists/${gistId}`, {
headers: {
'Authorization': `Bearer ${token}`,
'Accept': 'application/vnd.github.v3+json'
}
});
if (!response.ok) {
throw new Error(`GitHub API 错误: ${response.status}`);
}
const result = await response.json();
const content = result.files['bookmarks.json'].content;
return JSON.parse(content);
}
async function fetchFromGitee(token, gistId) {
const response = await fetch(`https://gitee.com/api/v5/gists/${gistId}?access_token=${token}`);
if (!response.ok) {
throw new Error(`Gitee API 错误: ${response.status}`);
}
const result = await response.json();
const content = result.files['bookmarks.json'].content;
return JSON.parse(content);
}
function countBookmarks(node) {
let count = 0;
if (node.url) {
count = 1;
}
if (node.children) {
for (const child of node.children) {
count += countBookmarks(child);
}
}
return count;
}
async function getRemoteBookmarkCount(platform) {
try {
const config = await chrome.storage.local.get([
'githubToken',
'giteeToken',
'githubGistId',
'giteeGistId'
]);
const token = platform === 'github' ? config.githubToken : config.giteeToken;
const gistId = platform === 'github' ? config.githubGistId : config.giteeGistId;
if (!gistId) {
return 0;
}
let bookmarkData;
if (platform === 'github') {
bookmarkData = await fetchFromGithub(token, gistId);
} else if (platform === 'gitee') {
bookmarkData = await fetchFromGitee(token, gistId);
}
return bookmarkData.count || 0;
} catch (error) {
console.error('获取远程书签数量失败:', error);
return 0;
}
}
async function findExistingGist(platform) {
try {
const config = await chrome.storage.local.get(['githubToken', 'giteeToken']);
if (!platform) {
throw new Error('未指定平台');
}
const token = platform === 'github' ? config.githubToken : config.giteeToken;
if (!token) {
throw new Error(`未配置 ${platform === 'github' ? 'GitHub' : 'Gitee'} Token`);
}
let gistList;
if (platform === 'github') {
gistList = await fetchGithubGists(token);
} else if (platform === 'gitee') {
gistList = await fetchGiteeGists(token);
}
const bookmarkCandidates = (gistList || []).filter(gist => {
return gist.files && gist.files['bookmarks.json'];
});
bookmarkCandidates.sort((a, b) => {
const ta = new Date(a.updated_at || a.created_at || 0).getTime();
const tb = new Date(b.updated_at || b.created_at || 0).getTime();
return tb - ta;
});
const bookmarkGist = bookmarkCandidates[0] || null;
if (bookmarkGist) {
let bookmarkData;
if (platform === 'github') {
bookmarkData = await fetchFromGithub(token, bookmarkGist.id);
} else {
bookmarkData = await fetchFromGitee(token, bookmarkGist.id);
}
const storageData = {};
if (platform === 'github') {
storageData.githubGistId = bookmarkGist.id;
storageData.githubRemoteCount = bookmarkData.count || 0;
} else {
storageData.giteeGistId = bookmarkGist.id;
storageData.giteeRemoteCount = bookmarkData.count || 0;
}
await chrome.storage.local.set(storageData);
return {
gistId: bookmarkGist.id,
count: bookmarkData.count || 0
};
} else {
return {
gistId: null,
count: 0
};
}
} catch (error) {
console.error('查找已存在的 Gist 失败:', error);
throw error;
}
}
async function fetchGithubGists(token) {
const response = await fetch('https://api.github.com/gists', {
headers: {
'Authorization': `Bearer ${token}`,
'Accept': 'application/vnd.github.v3+json'
}
});
if (!response.ok) {
throw new Error(`GitHub API 错误: ${response.status}`);
}
return await response.json();
}
async function fetchGiteeGists(token) {
const response = await fetch(`https://gitee.com/api/v5/gists?access_token=${token}`);
if (!response.ok) {
throw new Error(`Gitee API 错误: ${response.status}`);
}
return await response.json();
}
async function deleteGithubGist(token, gistId) {
const response = await fetch(`https://api.github.com/gists/${gistId}`, {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${token}`,
'Accept': 'application/vnd.github.v3+json'
}
});
if (!response.ok && response.status !== 404) {
const t = await response.text().catch(() => '');
throw new Error(`GitHub 删除 Gist 失败: ${response.status} ${t.slice(0, 120)}`);
}
}
async function deleteGiteeGist(token, gistId) {
const response = await fetch(
`https://gitee.com/api/v5/gists/${gistId}?access_token=${encodeURIComponent(token)}`,
{ method: 'DELETE' }
);
if (!response.ok && response.status !== 404) {
const t = await response.text().catch(() => '');
throw new Error(`Gitee 删除 Gist 失败: ${response.status} ${t.slice(0, 120)}`);
}
}
async function cleanupOldGists(platform) {
try {
const config = await chrome.storage.local.get([
'githubToken', 'giteeToken', 'githubGistId', 'giteeGistId'
]);
const token = platform === 'github' ? config.githubToken : config.giteeToken;
if (!token) return;
const list = platform === 'github'
? await fetchGithubGists(token)
: await fetchGiteeGists(token);
const candidates = (list || []).filter(g => g.files && g.files['bookmarks.json']);
if (candidates.length <= 1) return;
candidates.sort((a, b) => {
const ta = new Date(a.updated_at || a.created_at || 0).getTime();
const tb = new Date(b.updated_at || b.created_at || 0).getTime();
return tb - ta;
});
const keep = candidates[0];
const remove = candidates.slice(1);
let deleted = 0;
for (const g of remove) {
try {
if (platform === 'github') await deleteGithubGist(token, g.id);
else await deleteGiteeGist(token, g.id);
deleted++;
console.log(`[${platform}] 已删除旧片段: ${g.id}`);
} catch (e) {
console.warn(`[${platform}] 删除旧片段 ${g.id} 失败:`, e);
}
}
const storedId = platform === 'github' ? config.githubGistId : config.giteeGistId;
if (storedId !== keep.id) {
const set = platform === 'github'
? { githubGistId: keep.id }
: { giteeGistId: keep.id };
await chrome.storage.local.set(set);
console.log(`[${platform}] 更新本地记录 Gist 为最新: ${keep.id}`);
}
console.log(`[${platform}] 清理完成:保留 ${keep.id},删除 ${deleted} 个旧片段`);
} catch (e) {
console.warn(`[${platform}] 清理旧片段失败:`, e);
}
}
async function cleanupOnStartup() {
const config = await chrome.storage.local.get([
'autoCleanupOldGists', 'githubToken', 'giteeToken'
]);
if (config.autoCleanupOldGists === false) return;
const platforms = [];
if (config.githubToken) platforms.push('github');
if (config.giteeToken) platforms.push('gitee');
for (const p of platforms) {
await cleanupOldGists(p);
}
}