// ==UserScript==
// @name 闲鱼收藏卖家提醒
// @namespace https://www.goofish.com/
// @version 1.5.2
// @description v1.5.2:性能优化——存储内存缓存、角标不重建、切断 MutationObserver 自触发死循环、接口解析挪到空闲帧、pageItems 合并写入;功能与 v1.5.1 一致(详情页推荐位同样标红)
// @author You
// @match *://www.goofish.com/*
// @match *://h5api.m.goofish.com/*
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_deleteValue
// @grant GM_registerMenuCommand
// @grant GM_xmlhttpRequest
// @connect *
// @run-at document-end
// @license MIT
// ==/UserScript==
(function () {
'use strict';
// ============================================================
// 【第一步】立刻创建状态指示灯 —— 不依赖任何其他代码
// 只要脚本在运行,右下角就一定有个点
// ============================================================
var statusDot = null;
var initError = null;
function createStatusDot() {
try {
if (!document.body) {
document.addEventListener('DOMContentLoaded', createStatusDot, { once: true });
return;
}
if (statusDot) return;
// 注入最小样式
var st = document.createElement('style');
st.textContent = '#xy-status-dot{position:fixed!important;bottom:16px!important;right:16px!important;z-index:2147483647!important;width:14px!important;height:14px!important;border-radius:50%!important;box-shadow:0 0 10px currentColor!important;cursor:pointer!important;transition:background .3s!important;border:2px solid #fff!important;}';
document.head.appendChild(st);
statusDot = document.createElement('div');
statusDot.id = 'xy-status-dot';
statusDot.style.background = '#ff9800';
statusDot.style.color = '#ff9800';
statusDot.title = '闲鱼收藏提醒脚本运行中(橙色=初始化中)\n点击查看调试信息';
statusDot.addEventListener('click', showDebugInfo);
document.body.appendChild(statusDot);
console.log('%c[闲鱼收藏提醒] 状态指示灯已创建 ✓', 'color:#4caf50;font-weight:bold;font-size:14px');
} catch (e) {
console.error('[闲鱼收藏提醒] 创建状态指示灯失败:', e);
initError = e;
}
}
function setStatus(color, title) {
try {
if (!statusDot) return;
statusDot.style.background = color;
statusDot.style.color = color;
if (title) statusDot.title = title;
} catch (e) { }
}
// 立即执行(document-end 时 body 应该存在,但保险起见)
createStatusDot();
// ============================================================
// 配置
// ============================================================
var CONFIG = {
FAV_KEY: 'xianyu_favorite_sellers',
VISIT_KEY: 'xianyu_visit_history',
CACHE_TTL: 3600000,
TOAST_DURATION: 10000,
MAX_VISIT: null,
};
// ============================================================
// v1.5.0:云端共享数据库配置(Supabase)
// 使用前请在油猴菜单里填好 URL 和 Anon Key,或直接改这里
// ============================================================
var CLOUD_CONFIG = {
ENABLED: true, // 云端同步总开关
SUPABASE_URL: '', // 例: https://xxxx.supabase.co
SUPABASE_ANON_KEY: '', // Supabase 的 anon public key
SYNC_INTERVAL: 86400000, // 同步间隔:24小时(毫秒,一天一次)
DEVICE_ID_KEY: 'xianyu_device_id',
SHARED_KEY: 'xianyu_shared_sellers',
SHARED_LAST_SYNC_KEY: 'xianyu_shared_last_sync',
CLOUD_ENABLED_KEY: 'xianyu_cloud_enabled',
UPLOADED_FAV_KEY: 'xianyu_uploaded_fav',
UPLOADED_VISIT_KEY: 'xianyu_uploaded_visit',
};
// ============================================================
// 工具函数
// ============================================================
function log() {
var args = Array.prototype.slice.call(arguments);
console.log.apply(console, ['%c[闲鱼收藏提醒]', 'color:#ff6b00;font-weight:bold'].concat(args));
}
function warn() {
var args = Array.prototype.slice.call(arguments);
console.warn.apply(console, ['%c[闲鱼收藏提醒]', 'color:#ff6b00;font-weight:bold'].concat(args));
}
function getUserId(url) {
try {
var u = new URL(url, location.href);
return u.searchParams.get('userId') || u.searchParams.get('user_id');
} catch (e) { return null; }
}
function isProfile(url) {
return /\/personal(\?|$)/.test(url) && !!getUserId(url);
}
// ---- v1.4.0 新增:商品详情页识别 ----
function isItemPage(url) {
try {
var u = new URL(url, location.href);
var p = u.pathname;
return (p === '/item' || p.indexOf('/item/') === 0) && !!u.searchParams.get('id');
} catch (e) { return false; }
}
function getItemId(url) {
try {
var u = new URL(url, location.href);
return u.searchParams.get('id');
} catch (e) { return null; }
}
// ---- v1.4.5 新增:列表页识别(搜索页 / 首页推荐流) ----
function isSearchPage(url) {
try {
var u = new URL(url, location.href);
var p = u.pathname;
return (p === '/search' || p.indexOf('/search/') === 0 || p === '/' || p === '/index');
} catch (e) { return false; }
}
function isCollectionPage(url) {
try { return /\/collection/.test(url); } catch (e) { return false; }
}
function getItemIdFromHref(href) {
try {
var u = new URL(href, location.href);
return u.searchParams.get('id');
} catch (e) { return null; }
}
// 识别列表/搜索类接口(返回商品列表,含 userId/sellerId + itemId)
function isListApi(url) {
if (!url) return false;
var l = String(url).toLowerCase();
if (l.indexOf('favorite') >= 0 || l.indexOf('favlist') >= 0 || l.indexOf('fav_list') >= 0 || l.indexOf('my_fav') >= 0) return false;
// v1.4.6 扩大:所有 h5api/mtop/api 接口,以及 search/feed/recommend/sug
return l.indexOf('/h5/') >= 0 || l.indexOf('mtop') >= 0 || l.indexOf('api') >= 0 || l.indexOf('search') >= 0 || l.indexOf('feed') >= 0 || l.indexOf('recommend') >= 0 || l.indexOf('sug') >= 0;
}
function timeAgo(ts) {
var d = Date.now() - ts, m = Math.floor(d / 60000);
if (m < 1) return '刚刚';
if (m < 60) return m + '分钟前';
var h = Math.floor(m / 60);
if (h < 24) return h + '小时前';
var day = Math.floor(h / 24);
if (day < 30) return day + '天前';
return new Date(ts).toLocaleDateString('zh-CN');
}
// ============================================================
// 存储
// ============================================================
// v1.5.2:存储读缓存(性能优化核心)
// 原来 FavStore/VisitStore/ItemUserStore 每读一次都是 GM_getValue + JSON.parse,
// 而一次高亮要遍历几十张卡、每卡读 3~5 次 → 单次高亮上百次解析,这是卡顿主因。
// 现在内存缓存 5 秒,写入时同步更新缓存,跨标签页最多延迟 5 秒。
var StoreCache = {
_c: {},
ttl: 5000,
get: function (name, loader) {
var e = this._c[name];
if (e && (Date.now() - e.ts) < this.ttl) return e.v;
var v = null;
try { v = loader(); } catch (err) { v = null; }
this._c[name] = { v: v, ts: Date.now() };
return v;
},
put: function (name, v) { this._c[name] = { v: v, ts: Date.now() }; },
drop: function (name) { delete this._c[name]; }
};
var FavStore = {
get: function () {
var v = StoreCache.get('fav', function () {
try {
var raw = GM_getValue(CONFIG.FAV_KEY, null);
if (!raw) return { data: {}, updatedAt: 0 };
return typeof raw === 'string' ? JSON.parse(raw) : raw;
} catch (e) { warn('读取收藏失败', e); return { data: {}, updatedAt: 0 }; }
});
return v || { data: {}, updatedAt: 0 };
},
set: function (d) {
var obj = { data: d, updatedAt: Date.now() };
StoreCache.put('fav', obj);
try { GM_setValue(CONFIG.FAV_KEY, JSON.stringify(obj)); log('收藏索引已更新:', Object.keys(d).length, '个卖家'); }
catch (e) { warn('保存收藏失败', e); }
},
check: function (id) { return this.get().data[id] || null; },
clear: function () { StoreCache.drop('fav'); try { GM_deleteValue(CONFIG.FAV_KEY); } catch (e) { } },
expired: function () { var s = this.get(); return !s.updatedAt || (Date.now() - s.updatedAt) > CONFIG.CACHE_TTL; }
};
var VisitStore = {
get: function () {
var v = StoreCache.get('visit', function () {
try {
var raw = GM_getValue(CONFIG.VISIT_KEY, null);
if (!raw) return {};
return typeof raw === 'string' ? JSON.parse(raw) : raw;
} catch (e) { warn('读取访问历史失败', e); return {}; }
});
return v || {};
},
set: function (d) { StoreCache.put('visit', d); try { GM_setValue(CONFIG.VISIT_KEY, JSON.stringify(d)); } catch (e) { warn('保存访问历史失败', e); } },
check: function (id) { return this.get()[id] || null; },
record: function (id) {
var all = this.get(), now = Date.now(), rec = all[id];
if (rec) { rec.visitCount = (rec.visitCount || 1) + 1; rec.lastVisit = now; rec.isRevisit = true; }
else { rec = { firstVisit: now, lastVisit: now, visitCount: 1, isRevisit: false }; }
all[id] = rec;
// v1.4.4:无上限,访问历史不再淘汰,永久保留全部记录
this.set(all);
return rec;
},
clear: function () { StoreCache.drop('visit'); try { GM_deleteValue(CONFIG.VISIT_KEY); } catch (e) { } }
};
// ---- v1.4.15:itemId -> userId 映射 ----
// 商品详情页/卖家主页访问时采集,搜索页反查
var ITEM_USER_KEY = 'xianyu_item_user_map';
var ItemUserStore = {
get: function () {
var v = StoreCache.get('itemuser', function () {
try {
var raw = GM_getValue(ITEM_USER_KEY, null);
return raw ? JSON.parse(raw) : {};
} catch (e) { return {}; }
});
return v || {};
},
set: function (d) { StoreCache.put('itemuser', d); try { GM_setValue(ITEM_USER_KEY, JSON.stringify(d)); } catch (e) { } },
record: function (itemId, userId) {
if (!itemId || !userId) return;
var all = this.get();
if (String(all[String(itemId)]) === String(userId)) return; // 未变化不写
all[String(itemId)] = String(userId);
this.set(all);
log('itemId→userId 映射已保存:', itemId, '→', userId);
},
// v1.5.2:主页一次采到几百个 itemId,逐个 record 会做几百次读+全量写,改成批量一次写
recordBatch: function (itemIds, userId) {
if (!itemIds || !itemIds.length || !userId) return 0;
var all = this.get(), changed = 0;
for (var i = 0; i < itemIds.length; i++) {
var id = String(itemIds[i]);
if (String(all[id]) !== String(userId)) { all[id] = String(userId); changed++; }
}
if (!changed) return 0;
this.set(all);
log('itemId→userId 批量保存:', changed, '条 →', userId);
return changed;
},
getUserId: function (itemId) { return this.get()[String(itemId)] || null; },
clear: function () { StoreCache.drop('itemuser'); try { GM_deleteValue(ITEM_USER_KEY); } catch (e) { } }
};
// ---- v1.4.16:sellerId(加密/平台ID) → userId(明文主页ID) 别名 ----
var SID_ALIAS_KEY = 'xianyu_sid_alias';
var SidAliasStore = {
get: function () {
var v = StoreCache.get('sidalias', function () {
try {
var raw = GM_getValue(SID_ALIAS_KEY, null);
return raw ? JSON.parse(raw) : {};
} catch (e) { return {}; }
});
return v || {};
},
set: function (d) { StoreCache.put('sidalias', d); try { GM_setValue(SID_ALIAS_KEY, JSON.stringify(d)); } catch (e) { } },
record: function (sellerId, userId) {
if (!sellerId || !userId) return;
if (String(sellerId) === String(userId)) return;
var all = this.get();
if (String(all[String(sellerId)]) === String(userId)) return; // 未变化不写
all[String(sellerId)] = String(userId);
this.set(all);
log('sellerId→userId 别名已保存:', sellerId, '→', userId);
},
resolve: function (sellerId) {
if (!sellerId) return null;
return this.get()[String(sellerId)] || null;
},
clear: function () { StoreCache.drop('sidalias'); try { GM_deleteValue(SID_ALIAS_KEY); } catch (e) { } }
};
// ============================================================
// v1.5.0:SharedStore —— 云端共享卖家数据本地缓存
// 数据格式: { sellerId: { favCount: N, visitCount: N, updatedAt: ts } }
// ============================================================
function getDeviceId() {
try {
var did = GM_getValue(CLOUD_CONFIG.DEVICE_ID_KEY, '');
if (did) return did;
// 生成随机设备ID(匿名,不包含任何个人信息)
did = 'dev_' + Date.now().toString(36) + '_' + Math.random().toString(36).substring(2, 12);
GM_setValue(CLOUD_CONFIG.DEVICE_ID_KEY, did);
return did;
} catch (e) { return 'dev_unknown'; }
}
function isCloudEnabled() {
try {
var v = GM_getValue(CLOUD_CONFIG.CLOUD_ENABLED_KEY, '');
if (v === '') return CLOUD_CONFIG.ENABLED;
return v === '1';
} catch (e) { return CLOUD_CONFIG.ENABLED; }
}
function setCloudEnabled(on) {
try { GM_setValue(CLOUD_CONFIG.CLOUD_ENABLED_KEY, on ? '1' : '0'); } catch (e) { }
}
var SharedStore = {
get: function () {
var v = StoreCache.get('shared', function () {
try {
var raw = GM_getValue(CLOUD_CONFIG.SHARED_KEY, null);
return raw ? JSON.parse(raw) : {};
} catch (e) { return {}; }
});
return v || {};
},
set: function (d) {
StoreCache.put('shared', d);
try { GM_setValue(CLOUD_CONFIG.SHARED_KEY, JSON.stringify(d)); } catch (e) { }
},
getSharedFavCount: function (sid) {
if (!sid) return 0;
var d = this.get();
var rec = d[String(sid)];
return rec ? (rec.favCount || 0) : 0;
},
getSharedVisitCount: function (sid) {
if (!sid) return 0;
var d = this.get();
var rec = d[String(sid)];
return rec ? (rec.visitCount || 0) : 0;
},
isSharedFav: function (sid) { return this.getSharedFavCount(sid) > 0; },
isSharedVisit: function (sid) { return this.getSharedVisitCount(sid) > 0; },
merge: function (records) {
// records: [{seller_id, fav_count, visit_count, updated_at}]
if (!records || !records.length) return 0;
var all = this.get();
var merged = 0;
records.forEach(function (r) {
var sid = String(r.seller_id || r.sellerId || '');
if (!sid) return;
all[sid] = {
favCount: r.fav_count || r.favCount || 0,
visitCount: r.visit_count || r.visitCount || 0,
updatedAt: r.updated_at || r.updatedAt || Date.now()
};
merged++;
});
this.set(all);
return merged;
},
count: function () { return Object.keys(this.get()).length; },
clear: function () { StoreCache.drop('shared'); try { GM_setValue(CLOUD_CONFIG.SHARED_KEY, '{}'); } catch (e) { } }
};
// ---- v1.5.0:云端同步 ----
function cloudRequest(method, path, body, onSuccess, onError) {
if (!CLOUD_CONFIG.SUPABASE_URL || !CLOUD_CONFIG.SUPABASE_ANON_KEY) {
if (onError) onError(new Error('Supabase 未配置'));
return;
}
var url = CLOUD_CONFIG.SUPABASE_URL.replace(/\/$/, '') + path;
GM_xmlhttpRequest({
method: method,
url: url,
headers: {
'apikey': CLOUD_CONFIG.SUPABASE_ANON_KEY,
'Authorization': 'Bearer ' + CLOUD_CONFIG.SUPABASE_ANON_KEY,
'Content-Type': 'application/json',
'Prefer': 'return=representation'
},
data: body ? JSON.stringify(body) : null,
onload: function (res) {
try {
var data = res.responseText ? JSON.parse(res.responseText) : null;
if (res.status >= 200 && res.status < 300) {
if (onSuccess) onSuccess(data, res.status);
} else {
warn('云端请求失败 [' + res.status + ']', res.responseText);
if (onError) onError(new Error('HTTP ' + res.status));
}
} catch (e) {
warn('云端响应解析失败', e);
if (onError) onError(e);
}
},
onerror: function (e) { warn('云端网络错误', e); if (onError) onError(e); },
timeout: 15000
});
}
// 上传本地收藏/浏览卖家ID到云端(增量:只传上次没传过的)
function syncUpload() {
if (!isCloudEnabled()) return;
if (!CLOUD_CONFIG.SUPABASE_URL) return;
var deviceId = getDeviceId();
// 收集本地所有收藏卖家ID
var favData = FavStore.get().data || {};
var allFavIds = Object.keys(favData);
// 收集本地所有浏览卖家ID
var visitData = VisitStore.get() || {};
var allVisitIds = Object.keys(visitData);
// 读取上次已上传的记录
var uploadedFav = {};
var uploadedVisit = {};
try {
var rawF = GM_getValue(CLOUD_CONFIG.UPLOADED_FAV_KEY, '{}');
uploadedFav = rawF ? JSON.parse(rawF) : {};
} catch (e) { }
try {
var rawV = GM_getValue(CLOUD_CONFIG.UPLOADED_VISIT_KEY, '{}');
uploadedVisit = rawV ? JSON.parse(rawV) : {};
} catch (e) { }
// 计算新增的ID(没传过的)
var newFavIds = [];
allFavIds.forEach(function (id) { if (!uploadedFav[id]) newFavIds.push(id); });
var newVisitIds = [];
allVisitIds.forEach(function (id) { if (!uploadedVisit[id]) newVisitIds.push(id); });
if (!newFavIds.length && !newVisitIds.length) {
log('云端上传: 无新增卖家,跳过');
return;
}
// 调用 Supabase RPC 函数 report_activity
cloudRequest('POST', '/rest/v1/rpc/report_activity', {
p_fav_ids: newFavIds,
p_visit_ids: newVisitIds,
p_device_id: deviceId
}, function (data) {
// 上传成功后,记录这些ID已上传
newFavIds.forEach(function (id) { uploadedFav[id] = Date.now(); });
newVisitIds.forEach(function (id) { uploadedVisit[id] = Date.now(); });
try { GM_setValue(CLOUD_CONFIG.UPLOADED_FAV_KEY, JSON.stringify(uploadedFav)); } catch (e) { }
try { GM_setValue(CLOUD_CONFIG.UPLOADED_VISIT_KEY, JSON.stringify(uploadedVisit)); } catch (e) { }
log('云端上传成功(增量): 新增收藏' + newFavIds.length + '个, 新增浏览' + newVisitIds.length + '个 | 累计已上传收藏' + Object.keys(uploadedFav).length + '个, 浏览' + Object.keys(uploadedVisit).length + '个');
}, function (err) {
warn('云端上传失败', err);
});
}
// 从云端下载共享卖家数据(增量)
function syncDownload() {
if (!isCloudEnabled()) return;
if (!CLOUD_CONFIG.SUPABASE_URL) return;
var lastSync = GM_getValue(CLOUD_CONFIG.SHARED_LAST_SYNC_KEY, '2000-01-01T00:00:00Z');
// 拉取最近更新的共享卖家(取最近500条,按更新时间倒序)
cloudRequest('GET', '/rest/v1/shared_sellers?select=seller_id,fav_count,visit_count,updated_at&order=updated_at.desc&limit=500',
null, function (data) {
if (Array.isArray(data)) {
var n = SharedStore.merge(data);
GM_setValue(CLOUD_CONFIG.SHARED_LAST_SYNC_KEY, new Date().toISOString());
log('云端下载成功: 合并' + n + '条共享卖家, 本地缓存共' + SharedStore.count() + '个');
}
}, function (err) {
warn('云端下载失败', err);
});
}
function syncNow() {
log('手动触发云端同步...');
doSync();
}
var syncTimer = null;
function shouldSyncNow() {
var last = 0;
try { last = parseInt(GM_getValue('xianyu_last_sync_time', '0')) || 0; } catch (e) { }
if (!last) return true;
return (Date.now() - last) >= CLOUD_CONFIG.SYNC_INTERVAL;
}
function markSynced() {
try { GM_setValue('xianyu_last_sync_time', String(Date.now())); } catch (e) { }
}
function doSync() {
syncUpload();
setTimeout(function () { syncDownload(); markSynced(); }, 1000);
}
function startSyncTimer() {
if (syncTimer) clearInterval(syncTimer);
if (!isCloudEnabled()) { log('云端同步已关闭'); return; }
if (!CLOUD_CONFIG.SUPABASE_URL) {
log('云端同步未配置 Supabase URL,跳过。请在油猴菜单中配置。');
return;
}
// 启动后5秒检查:距上次同步满24小时才同步
setTimeout(function () {
if (shouldSyncNow()) {
log('距上次同步已满24小时,开始同步...');
doSync();
} else {
var last = 0;
try { last = parseInt(GM_getValue('xianyu_last_sync_time', '0')) || 0; } catch (e) { }
var hoursLeft = ((CLOUD_CONFIG.SYNC_INTERVAL - (Date.now() - last)) / 3600000).toFixed(1);
log('距上次同步未满24小时(还剩' + hoursLeft + '小时),跳过自动同步');
}
}, 5000);
// 每小时检查一次,到点了就同步
syncTimer = setInterval(function () {
if (shouldSyncNow()) {
log('定时器触发:满24小时,开始同步...');
doSync();
}
}, 3600000);
log('云端同步已启动(每小时检查一次,满24小时才同步,刷新页面不会重复同步)');
}
// ---- v1.4.0 新增:当前商品信息(内存,随页面变化,不落盘) ----
// itemId -> {sellerId, title, price, ts}
var curItems = {};
// ---- v1.4.5 新增:列表页商品映射(内存) ----
// itemId -> {sellerId, title, price},来自列表/搜索接口捕获,用于列表页红框高亮
var pageItems = {};
var hlTimer = null;
// ============================================================
// 调试信息弹窗
// ============================================================
function showDebugInfo() {
try {
var url = location.href;
var profile = isProfile(url);
var itemPage = isItemPage(url);
var sid = getUserId(url);
var itemId = itemPage ? getItemId(url) : null;
var cur = itemId ? curItems[String(itemId)] : null;
var fav = FavStore.get();
var vis = VisitStore.get();
var fs = sid ? FavStore.check(sid) : null;
var vr = sid ? VisitStore.check(sid) : null;
var itemSid = cur ? cur.sellerId : null;
var itemFs = itemSid ? FavStore.check(itemSid) : null;
var itemInFav = itemId ? isItemInFav(itemId) : false;
var listPage = isSearchPage(url);
var hlCount = listPage ? document.querySelectorAll('[data-xy-marked="1"]').length : 0;
var msg = '【脚本调试信息 v1.4.13】\n\n';
msg += '当前URL: ' + url.substring(0, 120) + '\n';
msg += '是否卖家主页: ' + (profile ? '是 ✓' : '否 ✗') + '\n';
msg += '是否商品详情页: ' + (itemPage ? '是 ✓' : '否 ✗') + '\n';
msg += '是否列表页(搜索/首页): ' + (listPage ? '是 ✓' : '否 ✗') + '\n';
msg += '列表页红框数: ' + (listPage ? hlCount : '-') + '\n';
msg += '列表页商品映射: ' + Object.keys(pageItems).length + ' 条\n';
// v1.4.16:诊断映射和别名
var iuMap = ItemUserStore.get();
var saMap = SidAliasStore.get();
msg += 'ItemUserStore(itemId→userId): ' + Object.keys(iuMap).length + ' 条\n';
msg += 'SidAliasStore(sellerId→userId): ' + Object.keys(saMap).length + ' 条\n';
var iuSample = Object.keys(iuMap).slice(0, 3).map(function (k) { return k + '→' + iuMap[k]; });
if (iuSample.length) msg += ' 映射样例: ' + iuSample.join(', ') + '\n';
var saSample = Object.keys(saMap).slice(0, 3).map(function (k) { return k + '→' + saMap[k]; });
if (saSample.length) msg += ' 别名样例: ' + saSample.join(', ') + '\n';
var piKeys = Object.keys(pageItems).slice(0, 3);
if (piKeys.length) {
msg += ' pageItems 样例: ';
msg += piKeys.map(function (k) {
var v = pageItems[k];
return k + '(sid=' + v.sellerId + ',raw=' + (v.rawSellerId || '-') + ')';
}).join(' | ') + '\n';
}
msg += '提取sellerId: ' + (sid || '无') + '\n';
msg += '提取itemId: ' + (itemId || '无') + '\n\n';
msg += '收藏索引: ' + Object.keys(fav.data).length + ' 个卖家\n';
msg += '访问历史: ' + Object.keys(vis).length + ' 个卖家(无上限)\n';
// v1.4.13:批量任务已停用,不再显示
// msg += '批量任务已处理商家: ' + Object.keys(BatchDoneStore.get()).length + ' 个\n\n';
if (profile && sid) {
msg += '当前卖家收藏: ' + (fs ? '有 ' + fs.count + ' 件 ✓' : '无 ✗') + '\n';
msg += '当前卖家访问: ' + (vr ? '第 ' + vr.visitCount + ' 次' : '首次(未记录)') + '\n';
}
if (itemPage && itemId) {
msg += '商品页卖家ID: ' + (itemSid || '未识别到') + '\n';
msg += '商品页卖家收藏: ' + (itemFs ? '有 ' + itemFs.count + ' 件 ✓' : '无 ✗') + '\n';
msg += '该商品已在收藏夹: ' + (itemInFav ? '是 ✓' : '否 ✗') + '\n';
if (cur) msg += '当前商品: ¥' + (cur.price || '?') + ' · ' + (cur.title || itemId) + '\n';
}
if (initError) msg += '\n初始化错误: ' + initError.message + '\n';
msg += '\n--- 排查指引 ---\n';
msg += '1. 右下角橙色点 = 脚本正在初始化\n';
msg += '2. 绿色点 = 初始化完成,普通页面\n';
msg += '3. 蓝色点 = 已识别到卖家主页/商品详情页/列表页\n';
// v1.4.13:批量任务已停用
// msg += '4. 红色点 = 批量收藏任务正在运行\n';
msg += '4. 列表页命中卡片左上角常驻角标:橙⭐已收藏 / 蓝👀已浏览\n';
msg += '5. 首次进入卖家主页不会弹"再次访问"(第二次才弹)\n';
msg += '6. 收藏提醒需先访问"我的收藏"页面采集数据\n';
msg += '7. 商品详情页需等卖家信息加载完,脚本会自动重试\n';
msg += '8. 列表页/商品详情页「相关推荐」命中卡片左上角常驻角标:橙⭐已收藏 / 蓝👀已浏览;悬浮仍显示红框\n';
msg += '9. F12控制台筛选"闲鱼收藏提醒"看详细日志(含DOM/接口提取卖家数统计)';
alert(msg);
} catch (e) {
alert('调试信息生成失败: ' + e.message);
}
}
// ============================================================
// 提醒框 UI
// ============================================================
function injectReminderStyles() {
try {
if (document.getElementById('xy-reminder-style')) return;
var s = document.createElement('style');
s.id = 'xy-reminder-style';
s.textContent = '@keyframes xy-in{from{opacity:0;transform:translateY(-15px)}to{opacity:1;transform:translateY(0)}}@keyframes xy-out{from{opacity:1;transform:translateY(0)}to{opacity:0;transform:translateY(-15px)}}.xy-card{pointer-events:auto;border-radius:12px;padding:12px 18px;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;font-size:14px;color:#333;width:100%;box-sizing:border-box;animation:xy-in .3s ease-out;box-shadow:0 6px 20px rgba(0,0,0,.15);}.xy-card.xy-close{animation:xy-out .4s ease-in forwards;}';
document.head.appendChild(s);
} catch (e) { warn('注入样式失败', e); }
}
// v1.4.7 新增:列表页命中卡片的 hover 红框样式
// v1.4.12 新增:命中卡片左上角常驻角标(⭐已收藏橙 / 👀已浏览蓝),不用悬浮直接可见
function injectHoverStyles() {
try {
if (document.getElementById('xy-hover-style')) return;
var s = document.createElement('style');
s.id = 'xy-hover-style';
s.textContent = '.xy-hit-card{cursor:pointer;position:relative!important;}.xy-hit-card.xy-hit-card:hover{outline:3px solid #ff3b30!important;outline-offset:-3px!important;box-shadow:0 0 14px rgba(255,59,48,.45)!important;z-index:5;transition:none!important;}.xy-badge-box{position:absolute!important;top:6px!important;left:6px!important;z-index:9!important;display:flex!important;gap:4px!important;pointer-events:none!important;}.xy-badge{display:inline-block!important;padding:2px 8px!important;border-radius:10px!important;font-size:11px!important;line-height:1.5!important;color:#fff!important;box-shadow:0 1px 4px rgba(0,0,0,.3)!important;white-space:nowrap!important;}.xy-badge-fav{background:#ff9500!important;}.xy-badge-vis{background:#4a90d9!important;}';
document.head.appendChild(s);
} catch (e) { warn('注入hover样式失败', e); }
}
function getContainer() {
try {
var c = document.getElementById('xy-reminder-container');
if (c) return c;
if (!document.body) return null;
c = document.createElement('div');
c.id = 'xy-reminder-container';
c.style.cssText = 'position:fixed!important;top:70px!important;left:50%!important;transform:translateX(-50%)!important;z-index:2147483646!important;display:flex!important;flex-direction:column!important;gap:10px!important;align-items:center!important;width:calc(100% - 40px)!important;max-width:440px!important;pointer-events:none!important;';
document.body.appendChild(c);
return c;
} catch (e) { warn('创建容器失败', e); return null; }
}
function showCard(type, html) {
try {
injectReminderStyles();
var c = getContainer();
if (!c) { warn('容器不存在,延迟重试'); setTimeout(function () { showCard(type, html); }, 500); return; }
var old = c.querySelector('.xy-card[data-type="' + type + '"]');
if (old) old.remove();
var card = document.createElement('div');
card.className = 'xy-card';
card.dataset.type = type;
if (type === 'fav') {
card.style.background = 'linear-gradient(135deg,#fff5e6,#ffe8cc)';
card.style.border = '2px solid #ff9500';
} else {
card.style.background = 'linear-gradient(135deg,#e6f0ff,#cce0ff)';
card.style.border = '2px solid #4a90d9';
}
var icon = type === 'fav' ? '⭐' : '👀';
card.innerHTML = '
' + icon + ' ' + html + '
× ';
c.appendChild(card);
card.querySelector('.xy-x').addEventListener('click', function () { removeCard(card); });
if (CONFIG.TOAST_DURATION > 0) setTimeout(function () { removeCard(card); }, CONFIG.TOAST_DURATION);
} catch (e) { warn('显示提醒失败', e); }
}
function removeCard(card) {
try {
if (!card || card.classList.contains('xy-close')) return;
card.classList.add('xy-close');
setTimeout(function () {
if (card.parentNode) card.parentNode.removeChild(card);
var c = document.getElementById('xy-reminder-container');
if (c && c.children.length === 0) c.remove();
}, 400);
} catch (e) { }
}
function clearCards() {
try { var c = document.getElementById('xy-reminder-container'); if (c) c.remove(); } catch (e) { }
}
// ============================================================
// 收藏数据采集
// ============================================================
function extractSellers(text) {
var sellers = {};
try {
var json = JSON.parse(text);
function walk(o, d) {
if (!o || d > 20) return;
if (Array.isArray(o)) { for (var i = 0; i < o.length; i++) walk(o[i], d + 1); return; }
if (typeof o !== 'object') return;
var sid = o.userId || o.sellerId || o.seller_id || o.aliPayUserId || o.ownerId || o.sellerUserId;
var iid = o.itemId || o.item_id || o.feedId || o.targetId || o.id;
if (sid && iid && !isNaN(iid)) {
if (!sellers[sid]) sellers[sid] = { count: 0, items: [] };
var exists = sellers[sid].items.some(function (x) { return x.id === String(iid); });
if (!exists) {
sellers[sid].count++;
if (sellers[sid].items.length < 10) {
sellers[sid].items.push({ id: String(iid), title: o.title || o.itemTitle || '', price: o.price || '' });
}
}
}
for (var k in o) { if (k !== '__proto__' && k !== 'constructor') walk(o[k], d + 1); }
}
walk(json, 0);
} catch (e) { }
return sellers;
}
// ---- v1.4.0 新增:从商品接口响应中提取商品信息(sellerId/itemId/标题/价格) ----
function extractItemInfo(text) {
try {
var json = JSON.parse(text);
function walk(o, d) {
if (!o || d > 25) return;
if (Array.isArray(o)) { for (var i = 0; i < o.length; i++) walk(o[i], d + 1); return; }
if (typeof o !== 'object') return;
var sid = o.userId || o.sellerId || o.seller_id || o.aliPayUserId || o.ownerId || o.sellerUserId;
var iid = o.itemId || o.item_id || o.feedId || o.targetId || null;
var title = o.title || o.itemTitle || '';
var price = o.price || o.priceText || o.soldPrice || '';
if (sid && iid) {
curItems[String(iid)] = { sellerId: String(sid), title: String(title), price: String(price), ts: Date.now() };
// v1.4.15:只有接口返回的 iid 等于当前商品页 URL 的 itemId 才记录映射(避免推荐商品污染)
try {
var curIid = getItemId(location.href);
if (curIid && String(iid) === String(curIid)) {
ItemUserStore.record(iid, sid);
}
} catch (e) { }
}
for (var k in o) { if (k !== '__proto__' && k !== 'constructor') walk(o[k], d + 1); }
}
walk(json, 0);
} catch (e) { }
}
// ---- v1.4.5 新增:从列表/搜索接口响应中提取商品 sellerId 映射,用于列表页红框高亮 ----
// v1.5.2:先做字符串预筛,避免对没有商品字段的响应做整段 JSON.parse
function extractPageItems(text) {
try {
if (!text || text.length > 3000000) return;
if (text.indexOf('itemId') < 0 && text.indexOf('item_id') < 0) return;
var json = JSON.parse(text);
function walk(o, d) {
if (!o || d > 25) return;
if (Array.isArray(o)) { for (var i = 0; i < o.length; i++) walk(o[i], d + 1); return; }
if (typeof o !== 'object') return;
var uid = o.userId || o.user_id || o.aliPayUserId || o.ownerId || o.sellerUserId;
var sid = o.sellerId || o.seller_id;
// v1.4.17:搜索接口的 userId 不是卖家主页 ID,不在此处建立别名
var iid = o.itemId || o.item_id || o.feedId || o.targetId || null;
var title = o.title || o.itemTitle || '';
var price = o.price || o.priceText || o.soldPrice || '';
// v1.4.19:优先加密原值 sid,保留 rawSellerId 用于别名转换
var effectiveSid = sid || uid;
if (effectiveSid && iid) {
pageItems[String(iid)] = {
sellerId: String(effectiveSid),
rawSellerId: sid ? String(sid) : String(effectiveSid),
userId: uid ? String(uid) : null,
title: String(title),
price: String(price)
};
}
for (var k in o) { if (k !== '__proto__' && k !== 'constructor') walk(o[k], d + 1); }
}
walk(json, 0);
savePageItemsSoon();
scheduleHighlight();
} catch (e) { }
}
// v1.5.2:pageItems 每次接口返回都全量 JSON.stringify 写 sessionStorage,越滚越大
// 改为 2 秒合并写一次,并把条目上限压到 4000 条(超出丢最旧的)
var pageItemsSaveTimer = null;
function savePageItemsSoon() {
if (pageItemsSaveTimer) return;
pageItemsSaveTimer = setTimeout(function () {
pageItemsSaveTimer = null;
try {
var keys = Object.keys(pageItems);
if (keys.length > 4000) {
for (var i = 0; i < keys.length - 4000; i++) delete pageItems[keys[i]];
}
sessionStorage.setItem('xy_page_items', JSON.stringify(pageItems));
} catch (e) { }
}, 2000);
}
// v1.5.2:高亮最小间隔 —— 避免 MutationObserver/补标定时器把全页扫描打成高频循环
var lastHlRunAt = 0;
// 防抖:接口多次返回时合并高亮动作,等 DOM 渲染完再执行
function scheduleHighlight() {
try {
if (hlTimer) clearTimeout(hlTimer);
hlTimer = setTimeout(function () {
hlTimer = null;
var now = Date.now();
if (now - lastHlRunAt < 1200) return; // 1.2 秒内不重复全扫
lastHlRunAt = now;
highlightMatchedCards();
}, 600);
} catch (e) { }
}
function isFavApi(url) {
if (!url) return false;
var l = url.toLowerCase();
return ['favorite', 'favourite', 'collect', 'collection', 'favlist', 'fav_list', 'myfav', 'my_fav', 'idle.fav', 'idlefav', 'fav.item'].some(function (k) { return l.indexOf(k) >= 0; });
}
// ---- v1.4.0 新增:识别商品详情接口 ----
function isItemApi(url) {
if (!url) return false;
var l = String(url).toLowerCase();
if (l.indexOf('favorite') >= 0 || l.indexOf('favlist') >= 0 || l.indexOf('fav_list') >= 0 || l.indexOf('my_fav') >= 0) return false;
return (l.indexOf('/item') >= 0 || l.indexOf('item.detail') >= 0 || l.indexOf('itemdetail') >= 0 || l.indexOf('item/detail') >= 0) &&
(l.indexOf('detail') >= 0 || l.indexOf('sdk') >= 0 || l.indexOf('mtop') >= 0 || l.indexOf('id=') >= 0);
}
// v1.5.2:把大 JSON 解析挪到空闲帧,避免滚动加载时卡住渲染
function deferIdle(fn) {
try {
if (typeof requestIdleCallback === 'function') { requestIdleCallback(fn, { timeout: 2000 }); return; }
} catch (e) { }
setTimeout(fn, 0);
}
function hookXHR() {
try {
var oOpen = XMLHttpRequest.prototype.open;
var oSend = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.open = function (m, u) { this._xyu = u; return oOpen.apply(this, arguments); };
XMLHttpRequest.prototype.send = function () {
var self = this;
this.addEventListener('load', function () {
try {
var u = String(self._xyu || '');
if (isFavApi(u) && self.responseText) {
log('XHR捕获收藏接口:', u.substring(0, 120));
var s = extractSellers(self.responseText);
if (Object.keys(s).length) { var ex = FavStore.get().data; for (var k in s) ex[k] = s[k]; FavStore.set(ex); }
} else if (isItemApi(u) && self.responseText) {
log('XHR捕获商品接口:', u.substring(0, 120));
extractItemInfo(self.responseText);
} else if (isListApi(u) && self.responseText) {
log('XHR捕获列表接口:', u.substring(0, 120));
var xt = self.responseText;
deferIdle(function () { extractPageItems(xt); });
}
} catch (e) { warn('XHR处理失败', e); }
});
return oSend.apply(this, arguments);
};
log('XHR hook 完成');
} catch (e) { warn('XHR hook 失败', e); }
}
function hookFetch() {
try {
if (typeof window.fetch !== 'function') { log('无 fetch,跳过'); return; }
var o = window.fetch;
window.fetch = function () {
var args = arguments;
return o.apply(this, args).then(function (resp) {
try {
var url = typeof args[0] === 'string' ? args[0] : (args[0] && args[0].url);
if (isFavApi(url)) {
log('fetch捕获收藏接口:', String(url).substring(0, 120));
resp.clone().text().then(function (t) {
var s = extractSellers(t);
if (Object.keys(s).length) { var ex = FavStore.get().data; for (var k in s) ex[k] = s[k]; FavStore.set(ex); }
}).catch(function () { });
} else if (isItemApi(url)) {
log('fetch捕获商品接口:', String(url).substring(0, 120));
resp.clone().text().then(function (t) {
extractItemInfo(t);
}).catch(function () { });
} else if (isListApi(url)) {
log('fetch捕获列表接口:', String(url).substring(0, 120));
resp.clone().text().then(function (t) {
deferIdle(function () { extractPageItems(t); });
}).catch(function () { });
}
} catch (e) { warn('fetch处理失败', e); }
return resp;
});
};
log('fetch hook 完成');
} catch (e) { warn('fetch处理失败', e); }
}
// ============================================================
// 页面检测
// ============================================================
// ---- v1.4.0 新增:从商品页 DOM 中提取卖家ID(点卖家头像/名字通常跳到 /personal?userId=xxx) ----
function findSellerIdFromLinks() {
try {
var links = document.querySelectorAll('a[href*="/personal"]');
for (var i = 0; i < links.length; i++) {
var h = links[i].getAttribute('href') || '';
var m = h.match(/[?&](?:userId|user_id)=([^]+)/);
if (m) return decodeURIComponent(m[1]);
}
var links2 = document.querySelectorAll('a[href*="userId="], a[href*="user_id="]');
for (var j = 0; j < links2.length; j++) {
var h2 = links2[j].getAttribute('href') || '';
var m2 = h2.match(/[?&](?:userId|user_id)=([^]+)/);
if (m2) return decodeURIComponent(m2[1]);
}
} catch (e) { }
return null;
}
function findSellerIdFromScripts() {
try {
var scripts = document.querySelectorAll('script');
for (var j = 0; j < scripts.length && j < 40; j++) {
var t = scripts[j].textContent || '';
var m = t.match(/"(?:sellerId|sellerUserId|ownerId)"\s*:\s*"?(\d{5,})/);
if (m) return m[1];
}
} catch (e) { }
return null;
}
// ---- v1.4.0 新增:商品是否已在收藏夹(按商品ID匹配) ----
function isItemInFav(itemId) {
try {
var id = String(itemId);
var data = FavStore.get().data;
for (var k in data) {
var arr = data[k].items || [];
for (var i = 0; i < arr.length; i++) {
if (String(arr[i].id) === id) return true;
}
}
} catch (e) { }
return false;
}
// ---- v1.4.5 新增:列表页红框高亮 ----
// v1.4.8 精准定位:从商品链接向上找,第一个同时满足
// ①只含1个商品链接 ②包含图片 ③包含文本(标题/价格) 的元素 = 单个商品卡片
function findItemCardEl(link) {
try {
var el = link;
for (var i = 0; i < 8; i++) {
if (!el || el === document.body) break;
var itemLinks = el.querySelectorAll ? el.querySelectorAll('a[href*="/item"], a[href*="id="]') : [];
var hasImg = el.querySelector ? el.querySelector('img') : null;
var hasText = el.textContent && el.textContent.trim().length > 5;
if (itemLinks.length <= 1 && hasImg && hasText) return el;
el = el.parentElement;
}
// 兜底:返回 link 的父元素(非 body)
return (link.parentElement && link.parentElement !== document.body) ? link.parentElement : link;
} catch (e) { return link; }
}
// v1.4.6 新增:从卡片 DOM 内直接提取卖家ID(不依赖接口)
function extractSellerIdFromCard(card) {
try {
if (!card || !card.querySelectorAll) return null;
var links = card.querySelectorAll('a[href*="/personal"], a[href*="userId="], a[href*="user_id="]');
for (var i = 0; i < links.length; i++) {
var h = links[i].getAttribute('href') || '';
var m = h.match(/[?&](?:userId|user_id)=([^]+)/);
if (m) return decodeURIComponent(m[1]);
}
var ds = card.dataset || {};
if (ds.sellerId || ds.userId || ds.sellerid) return String(ds.sellerId || ds.userId || ds.sellerid);
} catch (e) { }
return null;
}
// 命中判断:商品本身在收藏夹,或该商品卖家在收藏夹 / 访问过其主页
function isItemMatched(itemId, sellerId) {
try {
if (isItemInFav(itemId)) return true;
if (sellerId) {
if (FavStore.check(sellerId)) return true;
if (VisitStore.check(sellerId)) return true;
// v1.5.0:共享数据判断
if (SharedStore.isSharedFav(sellerId)) return true;
if (SharedStore.isSharedVisit(sellerId)) return true;
}
} catch (e) { }
return false;
}
// v1.5.1:一个卡片可能对应多个卖家ID来源,逐个收集用于匹配
// 顺序:DOM 里的明文 userId(最可信)→ itemId 反查(主页/详情页采集)→ 加密 sellerId 的别名 → 加密原值(仅供共享库)
function collectSellerCandidates(itemId, card, encSid) {
var out = [];
function push(v) {
if (!v) return;
v = String(v);
if (out.indexOf(v) < 0) out.push(v);
}
try { push(extractSellerIdFromCard(card)); } catch (e) { }
try { push(ItemUserStore.getUserId(itemId)); } catch (e) { }
try { if (encSid) push(SidAliasStore.resolve(encSid)); } catch (e) { }
try { push(encSid); } catch (e) { }
return out;
}
// v1.5.1:列表页(搜索/首页)+ 商品详情页底部“相关推荐”卡片统一高亮
// 只认 itemId 匹配,向上找含 img 的父级挂载角标
function highlightMatchedCards() {
try {
var onItem = isItemPage(location.href);
var onSearch = isSearchPage(location.href);
if (!onItem && !onSearch) return; // 详情页也要跑,供底部相关推荐卡片使用
var curId = onItem ? getItemId(location.href) : null;
injectHoverStyles();
var links = document.querySelectorAll('a[href*="/item"], a[href*="id="]');
var total = 0, favHit = 0, visitHit = 0;
var t0 = (window.performance && performance.now) ? performance.now() : Date.now();
var done = []; // 同一张卡可能被多个链接命中,只处理一次
var favData = FavStore.get().data;
var favItemIds = {};
Object.keys(favData).forEach(function (k) {
(favData[k].items || []).forEach(function (it) {
favItemIds[String(it.id)] = true;
});
});
for (var i = 0; i < links.length; i++) {
var href = links[i].getAttribute('href') || '';
var itemId = getItemIdFromHref(href);
if (!itemId) continue;
// 详情页的主商品本身不重复标记(顶部已有提醒框)
if (onItem && curId && String(itemId) === String(curId)) continue;
total++;
var card = links[i];
for (var j = 0; j < 3; j++) {
if (!card || card === document.body) break;
if (card.querySelector('img')) break;
card = card.parentElement;
}
if (!card) continue;
if (done.indexOf(card) >= 0) continue;
done.push(card);
var info = pageItems[String(itemId)] || null;
var encSid = info ? (info.rawSellerId || info.sellerId) : null;
var candidates = collectSellerCandidates(itemId, card, encSid);
// 商品级:itemId 精确命中收藏
var isFavItem = !!favItemIds[String(itemId)];
// 卖家级:任一候选ID在收藏/访问历史/共享库里
var isFavSeller = false, isVisit = false, sharedFavCnt = 0, sharedVisitCnt = 0;
for (var c = 0; c < candidates.length; c++) {
var cand = candidates[c];
if (!isFavSeller && FavStore.check(cand)) isFavSeller = true;
if (!isVisit && VisitStore.check(cand)) isVisit = true;
var fc = SharedStore.getSharedFavCount(cand);
var vc = SharedStore.getSharedVisitCount(cand);
if (fc > sharedFavCnt) sharedFavCnt = fc;
if (vc > sharedVisitCnt) sharedVisitCnt = vc;
}
var isFav = isFavItem || isFavSeller;
var isSharedFav = !isFav && sharedFavCnt > 0;
var isSharedVisit = !isFav && !isVisit && sharedVisitCnt > 0;
// v1.5.2:之前命中过、现在不再命中的卡,把旧角标清掉,避免残留
if (!isFav && !isVisit && !isSharedFav && !isSharedVisit) {
if (card.dataset.xySig) {
delete card.dataset.xySig;
card.classList.remove('xy-hit-card');
var stale = card.querySelector('.xy-badge-box');
if (stale) stale.remove();
}
continue;
}
if (isFav) favHit++;
if (isVisit) visitHit++;
// v1.5.2:命中状态没变化就跳过 DOM 重建
// 原来每次高亮都删角标再重建,MutationObserver 又因此再次触发高亮 → 死循环空转
var sig = (isFav ? 'F' : '') + (isVisit ? 'V' : '') + (isSharedFav ? 'S' + sharedFavCnt : '');
if (card.dataset.xySig === sig && card.querySelector('.xy-badge-box')) continue;
card.dataset.xySig = sig;
var oldBadge = card.querySelector('.xy-badge-box');
if (oldBadge) oldBadge.remove();
if (!card.classList.contains('xy-hit-card')) {
card.classList.add('xy-hit-card');
card.dataset.xyMarked = '1';
}
var badgeBox = document.createElement('div');
badgeBox.className = 'xy-badge-box';
if (isFav) {
var b1 = document.createElement('span');
b1.className = 'xy-badge xy-badge-fav';
b1.textContent = '⭐已收藏';
badgeBox.appendChild(b1);
}
// v1.5.0:共享收藏徽章
if (isSharedFav) {
var b1s = document.createElement('span');
b1s.className = 'xy-badge xy-badge-shared';
b1s.textContent = '👥' + sharedFavCnt + '人收藏';
b1s.style.background = 'rgba(33,150,243,0.9)';
badgeBox.appendChild(b1s);
}
if (isVisit) {
var b2 = document.createElement('span');
b2.className = 'xy-badge xy-badge-vis';
b2.textContent = '👀已浏览';
badgeBox.appendChild(b2);
}
card.appendChild(badgeBox);
}
var t1 = (window.performance && performance.now) ? performance.now() : Date.now();
log((onItem ? '详情页相关推荐高亮' : '列表页高亮') + ': 共' + total + '件, 已收藏' + favHit + ', 已浏览' + visitHit +
' | 收藏商品' + Object.keys(favItemIds).length + '个, itemId映射' + Object.keys(ItemUserStore.get()).length + '条' +
' | 耗时' + Math.round(t1 - t0) + 'ms');
} catch (e) { warn('卡片高亮失败', e); }
}
// ---- v1.4.5 新增:列表页检测(含重试,等页面商品渲染完成) ----
// v1.4.6 新增:MutationObserver 监听新卡片出现(翻页/加载更多时自动标记)
var moObserver = null;
function setupMutationObserver() {
try {
if (moObserver) return;
moObserver = new MutationObserver(function (mutations) {
var need = false;
for (var i = 0; i < mutations.length; i++) {
var nodes = mutations[i].addedNodes;
if (!nodes || !nodes.length) continue;
// v1.5.2:忽略脚本自己插入的角标节点,否则「加角标→触发观察→重扫→再加角标」会无限循环
for (var j = 0; j < nodes.length; j++) {
var n = nodes[j];
if (!n || n.nodeType !== 1) continue;
if (n.id && String(n.id).indexOf('xy-') === 0) continue;
if (String(n.className || '').indexOf('xy-') >= 0) continue;
need = true; break;
}
if (need) break;
}
if (need) scheduleHighlight();
});
moObserver.observe(document.body, { childList: true, subtree: true });
log('MutationObserver 已启动');
} catch (e) { warn('MutationObserver 启动失败', e); }
}
function checkSearchPage(attempt) {
try {
if (attempt === 0) {
// v1.4.14:从 sessionStorage 恢复上次捕获的商品映射,不再直接清空
try {
var raw = sessionStorage.getItem('xy_page_items');
if (raw) { var p = JSON.parse(raw); if (p && typeof p === 'object') pageItems = p; }
} catch (e) { }
if (!pageItems) pageItems = {};
}
log('===== 列表页检测 =====');
setStatus('#2196f3', '列表页:标记收藏/访问过卖家的商品(红框)\n点击查看调试信息');
setupMutationObserver();
highlightMatchedCards();
// v1.5.2:重试从 5 轮降到 3 轮(后面交给 MutationObserver 补标,不必一直全扫)
if (attempt < 2) setTimeout(function () { checkSearchPage(attempt + 1); }, 1000);
} catch (e) { warn('列表页检测失败', e); }
}
// ---- v1.4.0 新增:商品详情页检测(重试机制等待页面数据加载) ----
function checkItemPage(attempt) {
try {
var url = location.href;
var itemId = getItemId(url);
if (!itemId) { clearCards(); setStatus('#4caf50', '闲鱼收藏提醒运行中\n当前:商品页(未取到ID)'); return; }
log('===== 商品详情页检测 =====');
log('URL:', url);
log('itemId:', itemId);
setStatus('#2196f3', '商品详情页:' + itemId + '\n点击查看调试信息');
// v1.5.1:详情页底部的“相关推荐/猜你喜欢”商品卡也标角标 + 悬浮红框
// 推荐位是异步渲染的,立即跑一次 + 多次补标,同时用 MutationObserver 兜住后续追加
try {
injectHoverStyles();
setupMutationObserver();
scheduleHighlight();
// 只在首轮安排补标,避免重试轮次堆叠定时器
if (attempt === 0) {
setTimeout(scheduleHighlight, 2500);
setTimeout(scheduleHighlight, 6000);
}
} catch (e) { warn('详情页相关推荐标红初始化失败', e); }
// 提取卖家:优先 DOM 链接(高置信),其次商品接口缓存
var sellerFromLink = findSellerIdFromLinks();
var sellerId = sellerFromLink;
var cur = curItems[String(itemId)];
if (!sellerId && cur && cur.sellerId) sellerId = cur.sellerId;
if (!sellerId) sellerId = findSellerIdFromScripts();
log('提取sellerId:', sellerId || '无');
// 卖家信息还没加载出来 → 延迟重试(商品页内容异步加载)
if (!sellerId) {
if (attempt < 6) {
setTimeout(function () { checkItemPage(attempt + 1); }, 500);
return;
}
log('多次尝试仍未提取到卖家信息,仅检查商品本身是否已收藏');
}
// 1) 卖家相关提醒:再次访问(只读不计数)+ 收藏提醒
if (sellerId) {
// v1.4.19:DOM明文userId优先,接口加密sellerId建立别名
var plainUid = findSellerIdFromLinks();
var curInfo = curItems[String(itemId)];
var encSid = curInfo ? curInfo.sellerId : null;
if (plainUid && encSid && String(plainUid) !== String(encSid)) {
SidAliasStore.record(encSid, plainUid);
}
var recordUid = plainUid || sellerId;
ItemUserStore.record(itemId, recordUid);
// 再次访问:只读访问历史,若之前进入过该卖家主页则弹提醒;不记录、不累计次数
var vr = VisitStore.check(sellerId);
if (vr && vr.visitCount >= 1) {
var rhtml = '再次访问:你已进入该卖家主页 ' + vr.visitCount + ' 次
首次:' + timeAgo(vr.firstVisit) + ' · 上次:' + timeAgo(vr.lastVisit) + '
卖家ID: ' + sellerId + ' · 商品页不累计次数
';
showCard('revisit', rhtml);
}
var fi = FavStore.check(sellerId);
if (fi) {
var items = '';
if (fi.items && fi.items.length) {
for (var i = 0; i < Math.min(3, fi.items.length); i++) {
var it = fi.items[i];
items += '¥' + (it.price || '?') + ' · ' + (it.title || '商品' + it.id) + '
';
}
}
var curLine = cur ? ('当前商品:¥' + (cur.price || '?') + ' · ' + (cur.title || itemId) + '
') : '';
var fhtml = '收藏提醒:该卖家有 ' + fi.count + ' 件商品在你的收藏中
' + curLine + items + '卖家ID: ' + sellerId + ' · 收藏更新于 ' + new Date(FavStore.get().updatedAt).toLocaleString('zh-CN') + '
';
showCard('fav', fhtml);
setStatus('#ff9500', '收藏提醒已触发(商品页)\n点击查看调试信息');
log('===== 商品页检测完成 =====');
return;
}
}
// 2) 当前商品本身已在收藏夹 → 弹提醒
if (isItemInFav(itemId)) {
var line = cur ? ('¥' + (cur.price || '?') + ' · ' + (cur.title || itemId)) : ('商品ID: ' + itemId);
var fhtml2 = '收藏提醒:该商品已在你的收藏夹中
' + line + '
商品ID: ' + itemId + '
';
showCard('fav', fhtml2);
setStatus('#ff9500', '商品已收藏提醒已触发\n点击查看调试信息');
} else {
setStatus('#4caf50', '商品页:该商品/卖家不在收藏中\n点击查看调试信息');
}
log('===== 商品页检测完成 =====');
} catch (e) {
warn('商品页检测失败:', e);
setStatus('#f44336', '检测出错:' + e.message);
}
}
// ---- 卖家主页检测(原逻辑) ----
// ---- v1.4.18:多选择器+HTML兜底采集主页商品 itemId ----
function collectProfileItemIds(sid, attempt) {
attempt = attempt || 0;
try {
var ids = new Set();
var links = document.querySelectorAll('a[href]');
for (var i = 0; i < links.length; i++) {
var h = links[i].getAttribute('href') || '';
var m = h.match(/[?&]id=(\d{5,})/);
if (m) ids.add(m[1]);
}
var els = document.querySelectorAll('[data-id], [data-item-id], [data-itemid], [data-item]');
for (var j = 0; j < els.length; j++) {
var ds = els[j].dataset || {};
var candidates = [ds.id, ds.itemId, ds.itemid, ds.item];
for (var c = 0; c < candidates.length; c++) {
if (candidates[c] && /^\d{5,}$/.test(String(candidates[c]))) ids.add(String(candidates[c]));
}
}
// v1.5.2:只有 DOM 一路都没采到才做整页 HTML 正则兜底(整页 innerHTML 很贵,不宜每轮都扫)
if (ids.size === 0) {
var html = document.documentElement.innerHTML || '';
var patterns = [
/"itemId"\s*:\s*"?(\d{5,})"?/g,
/"item_id"\s*:\s*"?(\d{5,})"?/g,
/"id"\s*:\s*"?(\d{5,})"?\s*,\s*"[^"]*title/g
];
for (var p = 0; p < patterns.length; p++) {
var re = patterns[p], mm, max = 100;
while ((mm = re.exec(html)) && max-- > 0) ids.add(mm[1]);
}
}
if (ids.size > 0) {
ItemUserStore.recordBatch(Array.from(ids), sid);
log('主页商品采集成功(第' + attempt + '次):' + ids.size + ' 个 itemId → userId ' + sid);
log(' 采集样例:', Array.from(ids).slice(0, 5).join(','));
} else {
log('主页商品采集为空(第' + attempt + '次),继续轮询');
if (attempt < 6) setTimeout(function () { collectProfileItemIds(sid, attempt + 1); }, 1200);
else warn('主页商品采集最终失败:主页可能完全异步渲染或选择器需再调');
}
} catch (e) { warn('采集主页商品失败:', e); }
}
function checkProfilePage() {
try {
var url = location.href;
var sid = getUserId(url);
if (!sid) { warn('未提取到userId:', url); setStatus('#f44336', '错误:未提取到sellerId'); return; }
log('===== 卖家主页检测 =====');
log('URL:', url);
log('sellerId:', sid);
setStatus('#2196f3', '卖家主页:' + sid + '\n点击查看调试信息');
// v1.4.16:多选择器+轮询采集主页商品 itemId
setTimeout(function () { collectProfileItemIds(sid, 0); }, 800);
// 访问历史
var vr = VisitStore.record(sid);
log('访问:', vr.isRevisit ? '再次访问,第' + vr.visitCount + '次' : '首次访问');
if (vr.isRevisit) {
var html = '再次访问:这是你第 ' + vr.visitCount + ' 次进入该卖家主页
首次:' + timeAgo(vr.firstVisit) + ' · 上次:' + timeAgo(vr.lastVisit) + '
卖家ID: ' + sid + '
';
showCard('revisit', html);
setStatus('#4a90d9', '再次访问提醒已触发\n点击查看调试信息');
}
// 收藏
var fi = FavStore.check(sid);
log('收藏:', fi ? '有' + fi.count + '件' : '无(或未采集)');
if (fi) {
var items = '';
if (fi.items && fi.items.length) {
for (var i = 0; i < Math.min(3, fi.items.length); i++) {
var it = fi.items[i];
items += '¥' + (it.price || '?') + ' · ' + (it.title || '商品' + it.id) + '
';
}
}
var fhtml = '收藏提醒:该卖家有 ' + fi.count + ' 件商品在你的收藏中
' + items + '卖家ID: ' + sid + ' · 收藏更新于 ' + new Date(FavStore.get().updatedAt).toLocaleString('zh-CN') + '
';
showCard('fav', fhtml);
setStatus('#ff9500', '收藏提醒已触发\n点击查看调试信息');
} else if (FavStore.expired()) {
log('收藏数据为空或过期,请访问"我的收藏"页面采集');
}
log('===== 检测完成 =====');
} catch (e) {
warn('页面检测失败:', e);
setStatus('#f44336', '检测出错:' + e.message);
}
}
// ============================================================
// v1.5.0:批量取消收藏面板(收藏页专用)
// ============================================================
var BatchFavPanel = {
init: false,
running: false,
paused: false,
stoppedByUser: false,
cancelled: 0,
total: 0,
failStreak: 0,
currentInterval: 800,
panel: null,
panelStatus: null,
panelCount: null,
panelBarFill: null,
panelInterval: null,
chkPrescan: null,
chkScroll: null,
chkFollow: null,
btnStart: null,
btnPause: null,
btnStop: null,
CONFIG: {
defaultInterval: 800, intervalMin: 200, intervalMax: 10000,
verifyTimeout: 4000, scrollWait: 1200, maxFailStreak: 6,
backoffFactor: 1.6, maxPrescanLoops: 80
},
LS: function (key, def) {
try { var v = localStorage.getItem('xycf:' + key); return v === null ? def : JSON.parse(v); } catch (e) { return def; }
},
LSS: function (key, v) { try { localStorage.setItem('xycf:' + key, JSON.stringify(v)); } catch (e) { } },
qsa: function (sel, root) { return Array.from((root || document).querySelectorAll(sel)); },
isVisible: function (el) { if (!el) return false; var r = el.getBoundingClientRect(); return r.width > 0 && r.height > 0; },
getCancelButtons: function () {
var self = this;
var TAGS = 'div,span,a,button,li,em';
var all = self.qsa(TAGS);
return all.filter(function (e) {
if ((e.textContent || '').trim() !== '取消收藏') return false;
var hasInner = self.qsa(TAGS, e).some(function (c) { return (c.textContent || '').trim() === '取消收藏'; });
return !hasInner && self.isVisible(e);
});
},
currentCount: function () { return this.getCancelButtons().length; },
sleep: function (ms) { return new Promise(function (r) { setTimeout(r, ms); }); },
scrollBottom: function () { window.scrollTo(0, document.body.scrollHeight); window.scrollTo(0, document.documentElement.scrollHeight); },
scrollTop: function () { window.scrollTo(0, 0); },
readInterval: function () {
var v = parseInt(this.panelInterval.value, 10);
if (isNaN(v)) return this.CONFIG.defaultInterval;
return Math.min(this.CONFIG.maxInterval, Math.max(this.CONFIG.intervalMin, v));
},
currentTitle: function (el) {
try {
var card = el.closest('[class*="feeds-item-wrap"], [class*="feedsItem"], [class*="item-card"]');
var t = card && card.querySelector('[class*="main-title"], [class*="title"], [class*="Title"]');
var s = (t ? t.textContent : (card ? card.textContent : '')).trim();
return s.length > 18 ? s.slice(0, 18) + '…' : s;
} catch (e) { return ''; }
},
build: function () {
if (this.init) return;
var self = this;
var panel = document.createElement('div');
panel.id = 'xycf-panel';
panel.style.cssText = 'position:fixed;right:16px;bottom:60px;z-index:2147483647;width:264px;' +
'background:#1f2329;color:#f5f6f7;border-radius:10px;box-shadow:0 6px 24px rgba(0,0,0,.35);' +
'font-family:"Microsoft YaHei",system-ui,sans-serif;font-size:12px;line-height:1.6;padding:0;' +
'box-sizing:border-box;user-select:none;';
var head = document.createElement('div');
head.style.cssText = 'display:flex;align-items:center;justify-content:space-between;padding:8px 12px;border-bottom:1px solid #2c3138;';
var title = document.createElement('span');
title.textContent = '批量取消收藏';
title.style.cssText = 'font-size:13px;font-weight:700;color:#ff8c00;';
var btnFold = document.createElement('button');
btnFold.textContent = '收起';
btnFold.style.cssText = 'border:none;background:transparent;color:#8a919b;font-size:11px;cursor:pointer;';
var bodyEl = document.createElement('div');
bodyEl.style.cssText = 'padding:10px 12px 12px;';
btnFold.addEventListener('click', function () {
var folded = bodyEl.style.display !== 'none';
bodyEl.style.display = folded ? 'none' : '';
btnFold.textContent = folded ? '展开' : '收起';
});
head.appendChild(title); head.appendChild(btnFold);
var panelBar = document.createElement('div');
panelBar.style.cssText = 'height:8px;background:#343a42;border-radius:4px;overflow:hidden;margin-bottom:6px;';
self.panelBarFill = document.createElement('div');
self.panelBarFill.style.cssText = 'height:100%;width:0%;background:linear-gradient(90deg,#ff9a3d,#ff6a00);transition:width .3s;';
panelBar.appendChild(self.panelBarFill);
bodyEl.appendChild(panelBar);
self.panelCount = document.createElement('div');
self.panelCount.style.cssText = 'margin-bottom:6px;color:#e8eaed;font-weight:600;';
self.panelStatus = document.createElement('div');
self.panelStatus.style.cssText = 'margin-bottom:8px;color:#9aa0a8;min-height:18px;word-break:break-all;';
bodyEl.appendChild(self.panelCount);
bodyEl.appendChild(self.panelStatus);
var row1 = document.createElement('div');
row1.style.cssText = 'display:flex;align-items:center;gap:6px;margin-bottom:6px;';
var lab1 = document.createElement('label');
lab1.textContent = '点击间隔(ms)';
self.panelInterval = document.createElement('input');
self.panelInterval.type = 'number';
self.panelInterval.value = String(self.LS('interval', self.CONFIG.defaultInterval));
self.panelInterval.min = String(self.CONFIG.intervalMin);
self.panelInterval.max = String(self.CONFIG.maxInterval);
self.panelInterval.style.cssText = 'width:78px;padding:3px 6px;border:1px solid #3a3f47;border-radius:4px;background:#111418;color:#fff;';
row1.appendChild(lab1); row1.appendChild(self.panelInterval);
bodyEl.appendChild(row1);
function mkRow(chk, key, label, def) {
chk.type = 'checkbox';
chk.checked = self.LS(key, def !== false);
chk.addEventListener('change', function () { self.LSS(key, chk.checked); });
var row = document.createElement('div');
row.style.cssText = 'display:flex;align-items:center;gap:6px;margin-bottom:4px;';
var l = document.createElement('label'); l.textContent = label;
row.appendChild(chk); row.appendChild(l);
return row;
}
self.chkPrescan = document.createElement('input');
bodyEl.appendChild(mkRow(self.chkPrescan, 'prescan', '先统计总数,再从头处理', true));
self.chkScroll = document.createElement('input');
bodyEl.appendChild(mkRow(self.chkScroll, 'scroll', '自动滚动加载更多', true));
self.chkFollow = document.createElement('input');
bodyEl.appendChild(mkRow(self.chkFollow, 'follow', '跟随进度滚动显示', false));
var row3 = document.createElement('div');
row3.style.cssText = 'display:flex;gap:8px;margin-top:8px;';
function btnStyle(bg, fg) { return 'flex:1;padding:5px 0;border:none;border-radius:5px;background:' + bg + ';color:' + fg + ';font-size:12px;cursor:pointer;font-weight:600;'; }
self.btnStart = document.createElement('button');
self.btnStart.textContent = '开始';
self.btnStart.style.cssText = btnStyle('#ff8c00', '#1a1205');
self.btnPause = document.createElement('button');
self.btnPause.textContent = '暂停';
self.btnPause.style.cssText = btnStyle('#3a3f47', '#fff');
self.btnStop = document.createElement('button');
self.btnStop.textContent = '停止';
self.btnStop.style.cssText = btnStyle('#3a3f47', '#fff');
self.btnPause.disabled = true; self.btnStop.disabled = true;
row3.appendChild(self.btnStart); row3.appendChild(self.btnPause); row3.appendChild(self.btnStop);
bodyEl.appendChild(row3);
self.btnStart.addEventListener('click', function () { self.startRun(); });
self.btnPause.addEventListener('click', function () { self.togglePause(); });
self.btnStop.addEventListener('click', function () { self.stopRun(); });
var note = document.createElement('div');
note.textContent = '需已登录;失败会自动减速重试。';
note.style.cssText = 'margin-top:8px;color:#6d737b;font-size:11px;';
bodyEl.appendChild(note);
panel.appendChild(head); panel.appendChild(bodyEl);
document.body.appendChild(panel);
self.panel = panel;
self.init = true;
self.updateUI();
self.setStatus('就绪。点击"开始"批量取消收藏');
},
setBusy: function (busy) {
this.btnStart.disabled = busy;
this.btnPause.disabled = !busy;
this.btnStop.disabled = !busy;
},
setStatus: function (t) { this.panelStatus.textContent = t; },
setProgress: function (pct) { this.panelBarFill.style.width = Math.max(0, Math.min(100, pct)) + '%'; },
updateUI: function () { this.panelCount.textContent = '已取消 ' + this.cancelled + ' / ' + (this.total || this.currentCount()); },
beep: function () {
try {
var ctx = new (window.AudioContext || window.webkitAudioContext)();
var o = ctx.createOscillator(); var g = ctx.createGain();
o.connect(g); g.connect(ctx.destination);
o.frequency.value = 880; g.gain.value = 0.15;
o.start(); setTimeout(function () { o.stop(); ctx.close(); }, 300);
} catch (e) { }
},
prescan: async function () {
var self = this;
var lastCount = -1; var stableRounds = 0;
for (var i = 0; i < self.CONFIG.maxPrescanLoops; i++) {
if (!self.running) return;
self.scrollBottom();
await self.sleep(self.CONFIG.scrollWait);
var c = self.currentCount();
self.setStatus('正在统计收藏总数…已加载 ' + c + ' 条');
if (c === lastCount) { stableRounds++; if (stableRounds >= 2) break; } else { stableRounds = 0; }
lastCount = c;
}
self.total = Math.max(0, lastCount);
self.scrollTop();
await self.sleep(400);
},
waitForDecrease: function (before) {
var self = this;
var start = Date.now();
return new Promise(function (resolve) {
(function poll() {
if (!self.running) { resolve(true); return; }
if (self.currentCount() < before) { resolve(true); return; }
if (Date.now() - start > self.CONFIG.verifyTimeout) { resolve(false); return; }
setTimeout(poll, 200);
})();
});
},
processOne: async function () {
var self = this;
var btns = self.getCancelButtons();
if (!btns.length) return 'none';
var target = btns[0];
if (self.chkFollow.checked) { try { target.scrollIntoView({ block: 'nearest' }); } catch (e) { } }
var t = self.currentTitle(target);
if (t) self.setStatus('正在处理:' + t);
var before = self.currentCount();
try { target.click(); } catch (e) { }
var ok = await self.waitForDecrease(before);
if (ok) {
self.cancelled += Math.max(0, before - self.currentCount());
self.failStreak = 0;
self.currentInterval = self.readInterval();
return 'ok';
}
self.failStreak++;
self.currentInterval = Math.min(self.CONFIG.maxInterval, Math.round(self.currentInterval * self.CONFIG.backoffFactor));
self.setStatus('点击未生效,减速至 ' + self.currentInterval + 'ms(连续 ' + self.failStreak + ' 次)');
return 'fail';
},
run: async function () {
var self = this;
self.running = true; self.paused = false; self.stoppedByUser = false;
self.cancelled = 0; self.failStreak = 0; self.total = 0;
self.currentInterval = self.readInterval();
self.setBusy(true); self.setProgress(0); self.updateUI();
var reason = '';
try {
if (self.chkPrescan.checked) {
await self.prescan();
if (!self.running) return;
self.setStatus('统计完成,共 ' + self.total + ' 个收藏,开始处理…');
} else {
self.total = self.currentCount();
}
self.setProgress(0); self.updateUI();
while (self.running) {
while (self.paused && self.running) { await self.sleep(200); }
if (!self.running) break;
var r = await self.processOne();
if (r === 'none') {
if (self.total > self.cancelled && self.chkScroll.checked) {
self.scrollBottom();
await self.sleep(self.CONFIG.scrollWait);
if (self.currentCount() > 0) continue;
}
reason = '处理完成:共取消 ' + self.cancelled + ' 个收藏';
break;
}
if (r === 'fail' && self.failStreak >= self.CONFIG.maxFailStreak) {
reason = '连续 ' + self.failStreak + ' 次未生效(可能触发风控),已自动停止';
break;
}
self.setProgress(self.total ? Math.round(self.cancelled / self.total * 100) : 100);
self.updateUI();
await self.sleep(self.currentInterval);
}
} catch (e) {
reason = '出错:' + (e && e.message ? e.message : e);
} finally {
self.running = false; self.paused = false;
self.setBusy(false);
if (self.stoppedByUser) reason = '已手动停止';
self.setStatus(reason + '(已取消 ' + self.cancelled + ' / ' + (self.total || self.currentCount()) + ')');
self.setProgress(self.total ? Math.round(self.cancelled / self.total * 100) : 0);
self.updateUI();
if (self.total > 0 && self.cancelled >= self.total) self.beep();
}
},
startRun: function () { if (!this.running) this.run(); },
togglePause: function () {
if (!this.running) return;
this.paused = !this.paused;
this.btnPause.textContent = this.paused ? '继续' : '暂停';
if (this.paused) this.setStatus('已暂停(点击"继续"恢复)');
},
stopRun: function () { this.stoppedByUser = true; this.running = false; }
};
function checkPage() {
try {
var url = location.href;
if (isCollectionPage(url)) {
BatchFavPanel.build();
setStatus('#4caf50', '收藏页:批量取消面板已加载\n点击右下角面板开始');
return;
}
if (isProfile(url)) { checkProfilePage(); return; }
if (isItemPage(url)) { checkItemPage(0); return; }
if (isSearchPage(url)) { checkSearchPage(0); return; }
clearCards();
setStatus('#4caf50', '闲鱼收藏提醒运行中\n当前:普通页面\n点击查看调试信息');
} catch (e) {
warn('页面检测失败:', e);
setStatus('#f44336', '检测出错:' + e.message);
}
}
// ============================================================
// SPA 路由监听
// ============================================================
function setupSpa() {
try {
var last = location.href;
function trigger() { if (location.href !== last) { last = location.href; log('URL变化:', last); checkPage(); } }
var op = history.pushState, or = history.replaceState;
history.pushState = function () { var r = op.apply(this, arguments); setTimeout(trigger, 150); return r; };
history.replaceState = function () { var r = or.apply(this, arguments); setTimeout(trigger, 150); return r; };
window.addEventListener('popstate', function () { setTimeout(trigger, 150); });
setInterval(trigger, 1000);
log('SPA监听已启动');
} catch (e) { warn('SPA监听失败', e); }
}
// ============================================================
// v1.4.9 新增:批量收藏任务模块(状态机持久化,整页跳转不丢任务)
// ============================================================
var BATCH_CONFIG = {
DONE_KEY: 'xianyu_batch_done_sellers',
STATE_KEY: 'xianyu_batch_task_state',
MINIMIZE_KEY: 'xianyu_batch_panel_min',
MIN_DELAY: 3000,
MAX_DELAY: 8000,
MAX_TASK_COUNT: 12,
MAX_CONSECUTIVE_FAIL: 3,
WAIT_ELEMENT_TIMEOUT: 15000
};
var batchPanel = null;
var batchLogBox = null;
var batchStartBtn = null;
// 任务状态持久化存储(跨整页刷新/跳转不丢)
var BatchStateStore = {
get: function () {
try {
var raw = GM_getValue(BATCH_CONFIG.STATE_KEY, null);
if (!raw) return { running: false };
return typeof raw === 'string' ? JSON.parse(raw) : raw;
} catch (e) { warn('读取批量任务状态失败', e); return { running: false }; }
},
set: function (state) {
try { GM_setValue(BATCH_CONFIG.STATE_KEY, JSON.stringify(state)); }
catch (e) { warn('保存批量任务状态失败', e); }
},
reset: function () {
try { GM_deleteValue(BATCH_CONFIG.STATE_KEY); } catch (e) {}
}
};
// 已处理商家存储(商家级去重)
var BatchDoneStore = {
get: function () {
try {
var raw = GM_getValue(BATCH_CONFIG.DONE_KEY, "{}");
return typeof raw === "string" ? JSON.parse(raw) : raw;
} catch (e) { warn("读取批量已处理商家失败", e); return {}; }
},
set: function (obj) {
try { GM_setValue(BATCH_CONFIG.DONE_KEY, JSON.stringify(obj)); }
catch (e) { warn("保存批量已处理商家失败", e); }
},
check: function (sellerId) { return !!this.get()[String(sellerId)]; },
markDone: function (sellerId) {
var all = this.get();
all[String(sellerId)] = { ts: Date.now() };
this.set(all);
},
clear: function () { try { GM_deleteValue(BATCH_CONFIG.DONE_KEY); } catch (e) {} }
};
function batchRandomDelay() {
return Math.floor(Math.random() * (BATCH_CONFIG.MAX_DELAY - BATCH_CONFIG.MIN_DELAY) + BATCH_CONFIG.MIN_DELAY);
}
// 批量日志(同时写UI和持久化state,刷新页面不丢日志)
function batchLog(text) {
log('[批量任务]', text);
var state = BatchStateStore.get();
state.log = state.log || [];
var t = new Date().toLocaleTimeString();
state.log.push('[' + t + '] ' + text);
if (state.log.length > 30) state.log = state.log.slice(-30);
BatchStateStore.set(state);
if (batchLogBox) {
batchLogBox.textContent = state.log.join('\n');
batchLogBox.scrollTop = batchLogBox.scrollHeight;
}
}
// 创建批量控制面板(左下角)
function createBatchPanel() {
try {
if (batchPanel) return;
if (!document.body) { setTimeout(createBatchPanel, 500); return; }
var st = document.createElement('style');
st.textContent = '#xy-batch-panel{position:fixed;bottom:40px;left:16px;z-index:2147483645;background:#fff;border:1px solid #ddd;border-radius:8px;padding:10px;width:300px;box-shadow:0 4px 12px rgba(0,0,0,.15);font-size:12px;font-family:inherit;}#xy-batch-log{height:100px;overflow:auto;background:#f7f7f7;padding:6px;margin:6px 0;border-radius:4px;font-family:monospace;font-size:11px;white-space:pre-wrap;}#xy-batch-panel input{width:100%;box-sizing:border-box;padding:6px;border:1px solid #ddd;border-radius:4px;margin-bottom:6px;font-size:12px;}.xy-btns{display:flex;gap:4px;flex-wrap:wrap;margin-bottom:6px;}.xy-btns button{padding:4px 8px;font-size:11px;cursor:pointer;border:1px solid #ddd;border-radius:4px;background:#f5f5f5;}';
document.head.appendChild(st);
batchPanel = document.createElement('div');
batchPanel.id = 'xy-batch-panel';
batchPanel.innerHTML = '' +
'批量任务 v1.4.12 ' +
'— ' +
'
' +
'' +
'
' +
'
' +
'开始任务 ' +
'停止 ' +
'清空已处理 ' +
'
' +
'
运行日志:
' +
'
';
document.body.appendChild(batchPanel);
batchLogBox = batchPanel.querySelector('#xy-batch-log');
batchStartBtn = batchPanel.querySelector('#xy-batch-start');
// 恢复历史日志
var state = BatchStateStore.get();
if (state.log && state.log.length) {
batchLogBox.textContent = state.log.join('\n');
batchLogBox.scrollTop = batchLogBox.scrollHeight;
}
if (state.running) {
batchStartBtn.disabled = true;
setStatus('#f44336', '批量收藏任务运行中!\n请勿切换标签页/关闭页面');
}
// 按钮绑定
batchPanel.querySelector('#xy-batch-stop').addEventListener('click', function () {
var st = BatchStateStore.get();
st.running = false;
BatchStateStore.set(st);
batchLog('⏹️ 手动停止任务');
setStatus('#4caf50', '闲鱼收藏提醒运行中 ✓\n点击查看调试信息');
batchStartBtn.disabled = false;
});
batchPanel.querySelector('#xy-batch-clear').addEventListener('click', function () {
if (confirm('确认清空所有已处理商家记录?\n清空后下次任务会重复处理老商家。')) {
BatchDoneStore.clear();
batchLog('🗑️ 已清空已处理商家黑名单');
}
});
// 最小化/展开面板逻辑
var minBtn = batchPanel.querySelector('#xy-batch-min');
var bodyBox = batchPanel.querySelector('#xy-batch-body');
function applyMinState(isMin) {
if (isMin) {
bodyBox.style.display = 'none';
batchPanel.style.width = '120px';
minBtn.textContent = '+';
minBtn.title = '展开面板';
} else {
bodyBox.style.display = 'block';
batchPanel.style.width = '300px';
minBtn.textContent = '—';
minBtn.title = '最小化面板';
}
GM_setValue(BATCH_CONFIG.MINIMIZE_KEY, isMin ? '1' : '0');
}
minBtn.addEventListener('click', function () {
var isMin = bodyBox.style.display !== 'none';
applyMinState(isMin);
});
// 初始化恢复上次状态
var savedMin = GM_getValue(BATCH_CONFIG.MINIMIZE_KEY, '0');
applyMinState(savedMin === '1');
batchStartBtn.addEventListener('click', startBatchTask);
batchLog('批量面板初始化完成 ✓');
// 初始化完后,检查是否需要从上次的任务断点继续
setTimeout(checkBatchState, 3000);
} catch (e) { warn('批量面板创建失败', e); }
}
// 开始批量任务
function startBatchTask() {
try {
var st = BatchStateStore.get();
if (st.running) { alert('任务正在运行中!'); return; }
var kw = batchPanel.querySelector('#xy-batch-kw').value.trim();
if (!kw) { alert('请先输入搜索关键词!'); return; }
// 写入初始状态
var newState = {
running: true,
keyword: kw,
step: 'wait_search',
queue: [],
current_sid: null,
success: 0,
fail: 0,
consecutive_fail: 0,
log: []
};
BatchStateStore.set(newState);
batchStartBtn.disabled = true;
setStatus('#f44336', '批量收藏任务运行中!\n请勿切换标签页/关闭页面');
batchLog('▶️ 任务启动,关键词:' + kw);
// 跳转到搜索页
location.href = 'https://www.goofish.com/search?q=' + encodeURIComponent(kw);
} catch (e) {
warn('启动批量任务失败', e);
batchLog('💥 启动失败:' + e.message);
BatchStateStore.reset();
setStatus('#4caf50', '闲鱼收藏提醒运行中 ✓\n点击查看调试信息');
batchStartBtn.disabled = false;
}
}
// 状态机:每次页面加载后检查当前任务阶段,执行对应动作
function checkBatchState() {
var state = BatchStateStore.get();
if (!state.running) return; // 没跑任务就啥也不做
setStatus('#f44336', '批量收藏任务运行中!\n请勿切换标签页/关闭页面');
batchLog('🔄 恢复任务,当前阶段:' + state.step);
// 阶段1:搜索页,收集商家队列
if (state.step === 'wait_search') {
batchLog('⌛ 等待搜索结果加载...');
var searchTimer = setInterval(function () {
var curState = BatchStateStore.get();
if (!curState.running) { clearInterval(searchTimer); return; }
if (Object.keys(pageItems).length >= 8) {
clearInterval(searchTimer);
batchLog('✅ 搜索结果加载完成,共' + Object.keys(pageItems).length + '个商品');
var sellerSet = new Set();
var filteredDone = 0;
var filteredBad = 0;
for (var iid in pageItems) {
var sid = pageItems[iid].sellerId;
if (!sid) continue;
var sidStr = String(sid).trim();
// 过滤明显的乱码ID:带=、--、空格的非法字符串
if (/[=\-\s]/.test(sidStr) || sidStr.length < 5) { filteredBad++; continue; }
if (BatchDoneStore.check(sidStr)) { filteredDone++; continue; }
sellerSet.add(sidStr);
}
curState.queue = Array.from(sellerSet).slice(0, BATCH_CONFIG.MAX_TASK_COUNT);
batchLog('✅ 共' + Object.keys(pageItems).length + '个商品,过滤:' + filteredDone + '个已处理、' + filteredBad + '个非法ID');
batchLog('✅ 待处理商家:' + curState.queue.length + '个');
if (curState.queue.length === 0) {
batchLog('ℹ️ 没有新商家需要处理,任务结束');
return endBatchTask();
}
// 取第一个商家,跳他的主页
curState.current_sid = curState.queue.shift();
curState.step = 'visit_seller';
BatchStateStore.set(curState);
var delay = batchRandomDelay();
batchLog('⌛ 随机等待' + Math.floor(delay/1000) + '秒,跳转到卖家主页:' + curState.current_sid);
setTimeout(function () {
location.href = 'https://www.goofish.com/personal?userId=' + curState.current_sid;
}, delay);
}
}, 1000);
// 20秒超时
setTimeout(function () { clearInterval(searchTimer); }, 20000);
}
// 阶段2:卖家主页,找店铺第一个商品
else if (state.step === 'visit_seller') {
batchLog('⌛ 等待卖家主页商品加载...');
var sellerTimer = setInterval(function () {
var curState = BatchStateStore.get();
if (!curState.running) { clearInterval(sellerTimer); return; }
// 检测页面错误:不合法参数/页面不存在 直接跳过
var bodyText = document.body.innerText || '';
if (bodyText.indexOf('不合法的参数') >= 0 || bodyText.indexOf('页面不存在') >= 0 || bodyText.indexOf('页面出错') >= 0) {
clearInterval(sellerTimer);
batchLog('❌ 卖家ID无效,页面报参数错误,跳过');
curState.fail++;
curState.consecutive_fail++;
BatchStateStore.set(curState);
nextSeller();
return;
}
var links = document.querySelectorAll('a[href*="/item?id="]');
if (links.length > 0) {
clearInterval(sellerTimer);
var firstItemUrl = links[0].href;
batchLog('🔗 店铺第一个商品:' + firstItemUrl);
curState.step = 'fav_item';
curState.item_url = firstItemUrl;
BatchStateStore.set(curState);
var delay = batchRandomDelay();
batchLog('⌛ 随机等待' + Math.floor(delay/1000) + '秒,跳转到商品页');
setTimeout(function () {
location.href = firstItemUrl;
}, delay);
}
}, 1000);
setTimeout(function () {
clearInterval(sellerTimer);
var curState = BatchStateStore.get();
if (!curState.running) return;
batchLog('❌ 卖家主页未找到在售商品,跳过');
curState.fail++;
curState.consecutive_fail++;
BatchStateStore.set(curState);
nextSeller();
}, BATCH_CONFIG.WAIT_ELEMENT_TIMEOUT);
}
// 阶段3:商品页,点收藏按钮
else if (state.step === 'fav_item') {
batchLog('⌛ 等待收藏按钮加载...');
var favTimer = setInterval(function () {
var curState = BatchStateStore.get();
if (!curState.running) { clearInterval(favTimer); return; }
// 检测页面错误
var bodyText = document.body.innerText || '';
if (bodyText.indexOf('不合法的参数') >= 0 || bodyText.indexOf('页面不存在') >= 0 || bodyText.indexOf('页面出错') >= 0) {
clearInterval(favTimer);
batchLog('❌ 商品页报错,跳过');
curState.fail++;
curState.consecutive_fail++;
BatchStateStore.set(curState);
nextSeller();
return;
}
var btns = document.querySelectorAll('button, div[role="button"]');
var foundBtn = null;
for (var i = 0; i < btns.length; i++) {
var t = (btns[i].textContent || '').trim();
var aria = btns[i].getAttribute('aria-label') || '';
if ((t.indexOf('收藏') >= 0 || aria.indexOf('收藏') >= 0) && btns[i].offsetParent !== null) {
foundBtn = btns[i];
break;
}
}
if (foundBtn) {
clearInterval(favTimer);
batchLog('🖱️ 点击收藏按钮');
foundBtn.click();
setTimeout(function () {
var st2 = BatchStateStore.get();
if (!st2.running) return;
st2.success++;
st2.consecutive_fail = 0;
BatchDoneStore.markDone(st2.current_sid);
batchLog('✅ 商家' + st2.current_sid + '收藏成功,写入黑名单');
BatchStateStore.set(st2);
nextSeller();
}, 2000);
}
}, 1000);
setTimeout(function () {
clearInterval(favTimer);
var curState = BatchStateStore.get();
if (!curState.running) return;
batchLog('❌ 商品页未找到收藏按钮,跳过');
curState.fail++;
curState.consecutive_fail++;
BatchStateStore.set(curState);
nextSeller();
}, BATCH_CONFIG.WAIT_ELEMENT_TIMEOUT);
}
}
// 处理下一个商家
function nextSeller() {
var state = BatchStateStore.get();
if (!state.running) return;
// 检查连续失败
if (state.consecutive_fail >= BATCH_CONFIG.MAX_CONSECUTIVE_FAIL) {
batchLog('⚠️ 连续失败' + BATCH_CONFIG.MAX_CONSECUTIVE_FAIL + '次,自动停止任务(可能弹滑块/页面改版)');
return endBatchTask();
}
// 队列空了就结束
if (state.queue.length === 0) {
return endBatchTask();
}
// 取下一个
state.current_sid = state.queue.shift();
state.step = 'visit_seller';
BatchStateStore.set(state);
var delay = batchRandomDelay();
batchLog('⌛ 随机等待' + Math.floor(delay/1000) + '秒,处理下一个商家:' + state.current_sid);
setTimeout(function () {
location.href = 'https://www.goofish.com/personal?userId=' + state.current_sid;
}, delay);
}
// 任务结束
function endBatchTask() {
var state = BatchStateStore.get();
batchLog('🏁 任务结束!成功:' + state.success + '个,失败:' + state.fail + '个');
state.running = false;
BatchStateStore.set(state);
setStatus('#4caf50', '闲鱼收藏提醒运行中 ✓\n点击查看调试信息');
if (batchStartBtn) batchStartBtn.disabled = false;
}
// ============================================================
// 油猴菜单
// ============================================================
function setupMenu() {
try {
GM_registerMenuCommand('🧪 测试:弹出两种提醒框', function () {
showCard('fav', '【测试】收藏提醒:该卖家有 3 件商品在收藏中
¥99 · 测试商品A
¥199 · 测试商品B
测试提醒,10秒后消失
');
showCard('revisit', '【测试】再次访问:这是你第 5 次进入该卖家主页
首次:3天前 · 上次:2小时前
测试提醒
');
});
GM_registerMenuCommand('🧪 测试:商品页收藏提醒', function () {
showCard('fav', '【测试】收藏提醒:该卖家有 3 件商品在收藏中
当前商品:¥500 · 测试商品
¥99 · 测试商品A
测试提醒,10秒后消失
');
});
GM_registerMenuCommand('🔴 重新标记当前列表(常驻角标)', function () {
if (!isSearchPage(location.href) && !isItemPage(location.href)) { alert('请在搜索结果页、首页或商品详情页使用此功能。'); return; }
var marked = document.querySelectorAll('[data-xy-marked="1"]');
for (var i = 0; i < marked.length; i++) {
marked[i].dataset.xyMarked = '';
if (marked[i].classList) marked[i].classList.remove('xy-hit-card');
marked[i].style.outline = '';
marked[i].style.outlineOffset = '';
marked[i].style.boxShadow = '';
var oldBadge = marked[i].querySelector('.xy-badge-box');
if (oldBadge) oldBadge.parentNode.removeChild(oldBadge);
}
highlightMatchedCards();
var cnt = document.querySelectorAll('[data-xy-marked="1"]').length;
alert('已重新标记当前页面,共 ' + cnt + ' 个商品卡片命中(左上角常驻角标,悬浮时外框变红)。\n(F12控制台可看详细统计)');
});
GM_registerMenuCommand('🔄 刷新收藏数据(先打开收藏页)', function () { FavStore.clear(); alert('已清除收藏缓存!\n请前往「我的」→「收藏」浏览一次。'); });
GM_registerMenuCommand('📊 查看收藏索引', function () {
var s = FavStore.get(), n = Object.keys(s.data).length;
if (!n) { alert('收藏索引为空,请先访问收藏页面。'); return; }
var m = '共 ' + n + ' 个收藏卖家(更新于 ' + new Date(s.updatedAt).toLocaleString('zh-CN') + '):\n\n';
var cnt = 0;
for (var k in s.data) { if (cnt++ >= 50) break; m += '• ' + k + ': ' + s.data[k].count + '件\n'; }
if (n > 50) m += '... 还有 ' + (n - 50) + ' 个';
alert(m);
});
GM_registerMenuCommand('👀 查看访问历史', function () {
var all = VisitStore.get(), n = Object.keys(all).length;
if (!n) { alert('暂无访问历史。'); return; }
var arr = [];
for (var k in all) arr.push([k, all[k]]);
arr.sort(function (a, b) { return b[1].lastVisit - a[1].lastVisit; });
var m = '共访问过 ' + n + ' 个卖家(无上限):\n\n';
for (var i = 0; i < Math.min(50, arr.length); i++) {
m += '• ' + arr[i][0] + '\n 访问' + arr[i][1].visitCount + '次 · 上次' + timeAgo(arr[i][1].lastVisit) + ' · 首次' + timeAgo(arr[i][1].firstVisit) + '\n';
}
if (n > 50) m += '\n... 还有 ' + (n - 50) + ' 个';
alert(m);
});
// ===== v1.4.13:批量收藏任务功能已停用(代码保留,不影响其他功能) =====
// GM_registerMenuCommand('🤖 批量任务:显示/隐藏控制面板', function () {
// if (!batchPanel) { createBatchPanel(); return; }
// batchPanel.style.display = batchPanel.style.display === 'none' ? 'block' : 'none';
// });
// GM_registerMenuCommand('🤖 批量任务:查看已处理商家数量', function () {
// var all = BatchDoneStore.get();
// alert('批量任务已成功处理商家总数:' + Object.keys(all).length + '个\n(这些商家下次跑任务会自动跳过)');
// });
GM_registerMenuCommand('🗑️ 清除收藏缓存', function () { if (confirm('确定清除收藏缓存?')) { FavStore.clear(); alert('已清除!'); } });
GM_registerMenuCommand('🗑️ 清除访问历史', function () { if (confirm('确定清除访问历史?')) { VisitStore.clear(); alert('已清除!'); } });
GM_registerMenuCommand('🧹 清除别名缓存(修复30个卖家映射到同一ID)', function () { if (confirm('清除 SidAliasStore 里的错误别名?\n清除后搜索页将只依赖 ItemUserStore 反查。')) { SidAliasStore.clear(); alert('别名缓存已清空!请刷新页面后重新访问卖家主页测试。'); } });
GM_registerMenuCommand('🔧 手动采集当前主页商品', function () { if (!isProfile(location.href)) { alert('请在卖家主页使用此功能。'); return; } var sid = getUserId(location.href); collectProfileItemIds(sid, 0); alert('已触发采集,请查看控制台日志(筛选"闲鱼收藏提醒")。'); });
// ---- v1.5.0:云端共享菜单 ----
GM_registerMenuCommand('☁️ ' + (isCloudEnabled() ? '关闭云端共享' : '开启云端共享'), function () {
var on = !isCloudEnabled();
setCloudEnabled(on);
if (on) { startSyncTimer(); alert('云端共享已开启!\n正在同步数据...'); }
else { if (syncTimer) { clearInterval(syncTimer); syncTimer = null; } alert('云端共享已关闭。\n你的数据将不再上传,也不会下载共享数据。'); }
});
GM_registerMenuCommand('☁️ 立即同步云端数据', function () {
if (!isCloudEnabled()) { alert('云端共享当前是关闭状态,请先开启。'); return; }
if (!CLOUD_CONFIG.SUPABASE_URL) { alert('尚未配置 Supabase URL。\n请编辑脚本顶部 CLOUD_CONFIG 填入你的 Supabase 地址和 Key。'); return; }
syncNow();
alert('已触发同步!\n请查看控制台日志(筛选"闲鱼收藏提醒")确认结果。');
});
GM_registerMenuCommand('☁️ 查看云端配置状态', function () {
var msg = '【云端共享状态 v1.5.0】\n\n';
msg += '开关: ' + (isCloudEnabled() ? '✅ 已开启' : '❌ 已关闭') + '\n';
msg += 'Supabase URL: ' + (CLOUD_CONFIG.SUPABASE_URL ? '已配置' : '❌ 未配置') + '\n';
msg += '同步间隔: ' + (CLOUD_CONFIG.SYNC_INTERVAL / 3600000).toFixed(1) + '小时\n';
try { var uf = GM_getValue(CLOUD_CONFIG.UPLOADED_FAV_KEY, '{}'); var uv = GM_getValue(CLOUD_CONFIG.UPLOADED_VISIT_KEY, '{}'); msg += '已上传收藏卖家: ' + Object.keys(JSON.parse(uf||'{}')).length + ' 个\n'; msg += '已上传浏览卖家: ' + Object.keys(JSON.parse(uv||'{}')).length + ' 个\n'; } catch(e) {}
msg += '设备ID: ' + getDeviceId().substring(0, 20) + '...\n';
msg += '共享卖家缓存: ' + SharedStore.count() + ' 个\n';
msg += '上次同步: ' + (GM_getValue(CLOUD_CONFIG.SHARED_LAST_SYNC_KEY, '') || '从未') + '\n';
msg += '\n配置方法: 编辑脚本顶部 CLOUD_CONFIG\n填入你的 Supabase URL 和 anon key';
alert(msg);
});
GM_registerMenuCommand('☁️ 设置 Supabase 地址', function () {
var url = prompt('请输入 Supabase URL(例: https://xxxx.supabase.co):', CLOUD_CONFIG.SUPABASE_URL || '');
if (url === null) return;
CLOUD_CONFIG.SUPABASE_URL = url.trim();
try { GM_setValue('xianyu_supabase_url', CLOUD_CONFIG.SUPABASE_URL); } catch (e) { }
alert('URL 已设置(需刷新页面生效)');
});
GM_registerMenuCommand('☁️ 设置 Supabase Anon Key', function () {
var key = prompt('请输入 Supabase anon public key:', CLOUD_CONFIG.SUPABASE_ANON_KEY || '');
if (key === null) return;
CLOUD_CONFIG.SUPABASE_ANON_KEY = key.trim();
try { GM_setValue('xianyu_supabase_key', CLOUD_CONFIG.SUPABASE_ANON_KEY); } catch (e) { }
alert('Key 已设置(需刷新页面生效)');
});
log('菜单已注册');
} catch (e) { warn('菜单注册失败', e); }
}
// ============================================================
// 初始化(每个步骤独立 try-catch,互不影响)
// ============================================================
log('脚本启动 v1.5.2 | URL:', location.href, '| readyState:', document.readyState);
// v1.5.0:从本地存储读取 Supabase 配置
try {
var savedUrl = GM_getValue('xianyu_supabase_url', '');
var savedKey = GM_getValue('xianyu_supabase_key', '');
if (savedUrl) CLOUD_CONFIG.SUPABASE_URL = savedUrl;
if (savedKey) CLOUD_CONFIG.SUPABASE_ANON_KEY = savedKey;
} catch (e) { }
var steps = [
['XHR hook', hookXHR],
['fetch hook', hookFetch],
['SPA监听', setupSpa],
['油猴菜单', setupMenu],
['页面检测', checkPage],
['云端同步', startSyncTimer],
// v1.4.13:批量面板已停用
// ['批量面板', createBatchPanel],
];
var okCount = 0;
for (var i = 0; i < steps.length; i++) {
try {
steps[i][1]();
okCount++;
} catch (e) {
warn('步骤[' + steps[i][0] + ']失败:', e);
initError = e;
}
}
if (okCount === steps.length) {
setStatus('#4caf50', '闲鱼收藏提醒运行中 ✓\n点击查看调试信息');
log('全部初始化完成 ✓ (' + okCount + '/' + steps.length + ')');
} else {
setStatus('#ff9800', '部分功能初始化失败 (' + okCount + '/' + steps.length + ')\n点击查看调试信息');
warn('部分初始化失败:', okCount + '/' + steps.length);
}
})();