diff --git a/apps/backend/src/routes/pdd-extension.ts b/apps/backend/src/routes/pdd-extension.ts index 66ee1b3a..660ef1a4 100644 --- a/apps/backend/src/routes/pdd-extension.ts +++ b/apps/backend/src/routes/pdd-extension.ts @@ -97,7 +97,27 @@ router.post('/sync-cookie', async (req, res, next) => { }) } - // 3. 实测 Cookie 有效性(轻量探测) + // 3. 诊断与实测 Cookie 有效性(轻量探测) + const cookieKeys = decrypted.cookie + .split(';') + .map((part) => part.trim().split('=')[0]?.trim()) + .filter((k): k is string => Boolean(k)) + const hasPassId = cookieKeys.includes('PASS_ID') + const hasRckk = cookieKeys.includes('rckk') + + logInfo( + '[pdd-extension]', + `插件上报店铺 [${store.shopName}(${store.shopId})] Cookie,开始验证`, + { + shopId: store.shopId, + cookieLen: decrypted.cookie.length, + cookieKeyCount: cookieKeys.length, + hasPassId, + hasRckk, + cookieKeys: cookieKeys.slice(0, 15), + }, + ) + const testResult = await PddClient.testCookie({ cookie: decrypted.cookie, userAgent: decrypted.userAgent || store.userAgent, @@ -106,14 +126,31 @@ router.post('/sync-cookie', async (req, res, next) => { if (!testResult.valid) { logWarn('[pdd-extension]', `插件上报的 Cookie 实测未通过: ${testResult.message}`, { shopId: store.shopId, + hasPassId, + hasRckk, + cookieLen: decrypted.cookie.length, }) + + const isSessionExpired = + testResult.message.includes('43001') || + testResult.message.includes('会话已过期') || + !hasPassId + + const userGuidance = !hasPassId + ? 'Cookie 中未包含拼多多核心会话标识 (PASS_ID)。请在当前浏览器打开并登录 mms.pinduoduo.com 商家后台,再点击插件上传。' + : isSessionExpired + ? '拼多多商家会话已过期或失效 (43001)。请在浏览器刷新 mms.pinduoduo.com 页面重新登录,然后点击插件重新同步。' + : `Cookie 校验失败: ${testResult.message}` + res.status(400).json({ code: 400, - message: `Cookie 校验失败: ${testResult.message}`, + message: userGuidance, data: { valid: false, shopId: store.shopId, shopName: store.shopName, + hasPassId, + detail: testResult.message, }, }) return diff --git a/apps/backend/src/services/platforms/pdd/pdd-crypto-service.test.ts b/apps/backend/src/services/platforms/pdd/pdd-crypto-service.test.ts index cb95401b..27bf334c 100644 --- a/apps/backend/src/services/platforms/pdd/pdd-crypto-service.test.ts +++ b/apps/backend/src/services/platforms/pdd/pdd-crypto-service.test.ts @@ -40,12 +40,12 @@ test('decryptPddExtensionPayload successfully decrypts valid envelope', () => { assert.equal(decrypted.nonce, payload.nonce) }) -test('decryptPddExtensionPayload rejects expired timestamp (> 60s)', () => { +test('decryptPddExtensionPayload rejects expired timestamp (> 300s)', () => { const kp = getOrCreatePddRsaKeyPair() const payload = { shopId: 'store_test_01', cookie: 'PASS_ID=test_pass;', - timestamp: Date.now() - 120_000, // 2 minutes ago + timestamp: Date.now() - 600_000, // 10 minutes ago (> 300s) nonce: 'nonce-expired-' + Math.random().toString(36).slice(2), } diff --git a/apps/backend/src/services/platforms/pdd/pdd-crypto-service.ts b/apps/backend/src/services/platforms/pdd/pdd-crypto-service.ts index 055af7fa..ea596992 100644 --- a/apps/backend/src/services/platforms/pdd/pdd-crypto-service.ts +++ b/apps/backend/src/services/platforms/pdd/pdd-crypto-service.ts @@ -52,10 +52,10 @@ export interface PddDecryptedCookiePayload { nonce: string } -// In-memory Nonce cache for anti-replay attacks (TTL: 3 minutes) +// In-memory Nonce cache for anti-replay attacks (TTL: 6 minutes) const seenNonces = new Map() -const NONCE_TTL_MS = 180_000 -const MAX_TIMESTAMP_SKEW_MS = 60_000 +const NONCE_TTL_MS = 360_000 +const MAX_TIMESTAMP_SKEW_MS = 300_000 function cleanupExpiredNonces() { const now = Date.now() diff --git a/apps/extensions/pdd-cookie-sync/README.md b/apps/extensions/pdd-cookie-sync/README.md index fd05f692..94576989 100644 --- a/apps/extensions/pdd-cookie-sync/README.md +++ b/apps/extensions/pdd-cookie-sync/README.md @@ -57,6 +57,19 @@ - 客服日常只需像往常一样在浏览器中登录拼多多商家后台(`mms.pinduoduo.com`); - 只要商家后台处于登录状态,插件会在后台自动完成 Cookie 刷新与加密上报,服务端同步生效,全程无感。 - **角标显示红色 ERR**: - - 代表配对密钥错误或接口网络受阻,点击插件图标可查看具体错误原因。 -- **角标显示黄色 OFF / NO_CK**: + - 点击插件图标查看具体错误原因。 + - 若提示「会话已过期 (43001)」或「未检测到 PASS_ID」:请在当前浏览器中打开 `mms.pinduoduo.com` 重新登录商家后台,然后点击插件【立即上传同步 Cookie】。 +- **角标显示黄色 OFF / NO_CK / 未登录**: - 代表当前浏览器未登录拼多多商家后台,只需在网页中正常登录即可自动恢复。 + +--- + +## 诊断中心与运行日志(v1.1.0 新增) + +1. **实时日志控制台**: + - 插件界面下方自带【运行日志】面板,记录包括 Cookie 嗅探、公钥获取、加密打包、服务端响应状态在内的全链路日志; + - 关键令牌安全脱敏显示,敏感信息不落地、不泄露。 +2. **一键复制日志**: + - 遇到任何异常时,点击【复制日志】即可将诊断上下文完整复制至剪贴板,方便快速排查; +3. **手动指定 Cookie (选填)**: + - 针对多浏览器环境或特定隔离场景,插件配置区支持手动粘贴 Cookie 覆盖,默认留空即采用全自动多维嗅探。 diff --git a/apps/extensions/pdd-cookie-sync/background.js b/apps/extensions/pdd-cookie-sync/background.js index bcb43ca0..ea131a40 100644 --- a/apps/extensions/pdd-cookie-sync/background.js +++ b/apps/extensions/pdd-cookie-sync/background.js @@ -1,6 +1,6 @@ /** * Pinduoduo Cookie Sync Background Service Worker (Manifest V3) - * 拼多多 Cookie 自动嗅探与加密保活后台服务 + * 拼多多 Cookie 自动嗅探与加密保活后台服务 - 带结构化日志系统 */ try { @@ -10,6 +10,7 @@ try { } const ALARM_NAME = 'pdd_cookie_sync_alarm' +const MAX_LOGS_LIMIT = 100 let debounceTimer = null // 设置扩展图标角标状态 @@ -20,18 +21,161 @@ function setExtensionBadge(text, color) { } } -// 获取拼多多全量 Cookie 字符串 -async function getPddCookieString() { +// 格式化时间戳 +function getNowFormattedTime() { + const d = new Date() + const m = String(d.getMonth() + 1).padStart(2, '0') + const day = String(d.getDate()).padStart(2, '0') + const h = String(d.getHours()).padStart(2, '0') + const min = String(d.getMinutes()).padStart(2, '0') + const s = String(d.getSeconds()).padStart(2, '0') + return `${m}-${day} ${h}:${min}:${s}` +} + +/** + * 记录扩展运行日志并持久化至 chrome.storage.local + * @param {'info' | 'warn' | 'error' | 'success'} level 日志级别 + * @param {string} action 动作模块 + * @param {string} message 简要说明 + * @param {any} [details] 附加诊断数据(脱敏安全显示) + */ +async function addExtensionLog(level, action, message, details = null) { try { - const cookies = await chrome.cookies.getAll({ domain: 'pinduoduo.com' }) - if (!cookies || cookies.length === 0) { - return '' + const timeStr = getNowFormattedTime() + const entry = { + id: 'log_' + Date.now() + '_' + Math.random().toString(36).slice(2, 6), + time: timeStr, + timestamp: Date.now(), + level, + action, + message, + details: details ? (typeof details === 'object' ? JSON.stringify(details) : String(details)) : null, + } + + const data = await chrome.storage.local.get(['pdd_extension_logs']) + const existing = Array.isArray(data.pdd_extension_logs) ? data.pdd_extension_logs : [] + const updated = [entry, ...existing].slice(0, MAX_LOGS_LIMIT) + await chrome.storage.local.set({ pdd_extension_logs: updated }) + + console.log(`[PDD-EXT] [${level.toUpperCase()}] [${action}] ${message}`, details || '') + } catch (err) { + console.error('保存扩展日志异常:', err) + } +} + +/** + * 获取拼多多全量 Cookie 字符串 + * 针对 mms.pinduoduo.com Host-only Cookie、根域名 Cookie、不同路径 Cookie 进行多维深度嗅探与合并去重 + */ +async function getPddCookieDetails(customCookie = '') { + if (customCookie && customCookie.trim().length > 10) { + const trimmed = customCookie.trim() + const keys = trimmed + .split(';') + .map((p) => p.trim().split('=')[0]?.trim()) + .filter(Boolean) + return { + cookieStr: trimmed, + cookieCount: keys.length, + keys, + hasPassId: keys.includes('PASS_ID'), + hasRckk: keys.includes('rckk'), + hasNano: keys.some((k) => k.includes('nano')), + isManual: true, + } + } + + try { + const cookieMap = new Map() + + // 1. 深度嗅探 MMS 商家后台关键 URL 上下文 (包含 Host-only、Secure、HttpOnly 令牌) + const targetUrls = [ + 'https://mms.pinduoduo.com/', + 'https://mms.pinduoduo.com/orders/list', + 'https://mms.pinduoduo.com/home/', + 'https://mms.pinduoduo.com/mangkhut/mms/recentOrderList', + ] + + for (const url of targetUrls) { + try { + const cookies = await chrome.cookies.getAll({ url }) + if (Array.isArray(cookies)) { + for (const c of cookies) { + if (c.name && c.value) { + cookieMap.set(c.name, c.value) + } + } + } + } catch (err) { + console.warn(`读取 URL [${url}] Cookie 异常:`, err) + } + } + + // 2. 补充查询主域名及各子域 Cookie + const domains = [ + 'mms.pinduoduo.com', + '.mms.pinduoduo.com', + 'pinduoduo.com', + '.pinduoduo.com', + ] + + for (const domain of domains) { + try { + const cookies = await chrome.cookies.getAll({ domain }) + if (Array.isArray(cookies)) { + for (const c of cookies) { + if (c.name && c.value && !cookieMap.has(c.name)) { + cookieMap.set(c.name, c.value) + } + } + } + } catch (err) { + console.warn(`读取 Domain [${domain}] Cookie 异常:`, err) + } + } + + const keys = Array.from(cookieMap.keys()) + const hasPassId = cookieMap.has('PASS_ID') + const hasRckk = cookieMap.has('rckk') + const hasNano = keys.some((k) => k.includes('nano')) + + if (cookieMap.size === 0) { + return { + cookieStr: '', + cookieCount: 0, + keys: [], + hasPassId: false, + hasRckk: false, + hasNano: false, + isManual: false, + } + } + + // 拼接成标准 Cookie 请求头字符串 + const cookieStr = Array.from(cookieMap.entries()) + .map(([name, val]) => `${name}=${val}`) + .join('; ') + + return { + cookieStr, + cookieCount: cookieMap.size, + keys, + hasPassId, + hasRckk, + hasNano, + isManual: false, } - // 拼接成标准 Cookie 请求头格式 - return cookies.map((c) => `${c.name}=${c.value}`).join('; ') } catch (err) { console.error('读取拼多多 Cookie 异常:', err) - return '' + return { + cookieStr: '', + cookieCount: 0, + keys: [], + hasPassId: false, + hasRckk: false, + hasNano: false, + isManual: false, + } } } @@ -41,6 +185,7 @@ async function syncCookieToServer(isManual = false) { 'serverUrl', 'shopId', 'syncToken', + 'manualCookie', 'lastCookieHash', 'lastSyncAt', ]) @@ -48,29 +193,66 @@ async function syncCookieToServer(isManual = false) { const serverUrl = String(config.serverUrl || '').trim().replace(/\/+$/, '') const shopId = String(config.shopId || '').trim() const syncToken = String(config.syncToken || '').trim() + const manualCookie = String(config.manualCookie || '').trim() if (!serverUrl || !shopId || !syncToken) { setExtensionBadge('CFG', '#faad14') // 待配置 + await addExtensionLog('warn', '配置检查', '未完成基础配置,缺少服务端地址、店铺标识或配对密钥') return { success: false, message: '请先打开插件弹窗完成服务器地址与店铺配对密钥配置', } } - // 1. 读取当前 Cookie - const cookieStr = await getPddCookieString() + await addExtensionLog( + 'info', + '发起同步', + `${isManual ? '手动点击立即同步' : '后台自动/保活同步'} [店铺: ${shopId}]`, + { serverUrl, shopId, isManual }, + ) + + // 1. 读取拼多多 Cookie + const cookieDetails = await getPddCookieDetails(manualCookie) + const cookieStr = cookieDetails.cookieStr + if (!cookieStr) { setExtensionBadge('OFF', '#8c8c8c') // 未登录 await chrome.storage.local.set({ lastStatus: 'offline', lastMessage: '未在浏览器中检测到拼多多商家后台登录 Cookie', }) + await addExtensionLog( + 'error', + 'Cookie嗅探', + '未在浏览器检测到任何拼多多 Cookie,请先在当前浏览器打开并登录 mms.pinduoduo.com', + ) return { success: false, - message: '未检测到拼多多登录 Cookie,请先在当前浏览器中登录 mms.pinduoduo.com', + message: '未检测到拼多多登录 Cookie,请先在当前浏览器中登录 mms.pinduoduo.com 商家后台', } } + await addExtensionLog( + cookieDetails.hasPassId ? 'info' : 'warn', + 'Cookie嗅探', + `成功提取 ${cookieDetails.cookieCount} 项 Cookie (含: ${cookieDetails.keys.slice(0, 8).join(', ')}${cookieDetails.keys.length > 8 ? '...' : ''})`, + { + totalKeys: cookieDetails.cookieCount, + hasPassId: cookieDetails.hasPassId, + hasRckk: cookieDetails.hasRckk, + hasNano: cookieDetails.hasNano, + isManual: cookieDetails.isManual, + }, + ) + + if (!cookieDetails.hasPassId) { + await addExtensionLog( + 'warn', + '会话预警', + '⚠️ 未检测到拼多多核心会话标识 PASS_ID,可能未登录或会话已过期。请在浏览器登录 mms.pinduoduo.com 并刷新后台页面', + ) + } + // 2. 计算 Cookie 哈希并进行指纹去重 const currentHash = await computeSha256(cookieStr) const now = Date.now() @@ -78,6 +260,7 @@ async function syncCookieToServer(isManual = false) { // 若非手动强制触发,且 Cookie 未变动,且距离上次同步不足 1 小时,跳过重复上报 if (!isManual && config.lastCookieHash === currentHash && now - lastSyncTime < 3600_000) { + await addExtensionLog('info', '指纹比对', 'Cookie 无变动且未到周期,跳过重复网络上报') return { success: true, skipped: true, @@ -88,22 +271,25 @@ async function syncCookieToServer(isManual = false) { try { // 3. 请求服务端公钥 const pkUrl = `${serverUrl}/api/v1/pdd/extension/public-key` + await addExtensionLog('info', '获取公钥', `请求服务端公钥: ${pkUrl}`) + const pkResp = await fetch(pkUrl, { method: 'GET', headers: { Accept: 'application/json' }, }) if (!pkResp.ok) { - throw new Error(`获取公钥失败 HTTP ${pkResp.status}`) + const statusText = await pkResp.text().catch(() => '') + throw new Error(`获取公钥失败 HTTP ${pkResp.status}: ${statusText || pkResp.statusText}`) } const pkJson = await pkResp.json() const serverPublicKeySpki = pkJson?.data?.publicKeySpki if (!serverPublicKeySpki) { - throw new Error('服务端返回的 RSA 公钥格式异常') + throw new Error('服务端返回的 RSA 公钥格式异常 (缺少 publicKeySpki 字段)') } - // 4. 构建明文载荷(syncToken 在加密包内外双重保护) + // 4. 构建明文载荷 const payload = { shopId, syncToken, @@ -114,10 +300,13 @@ async function syncCookieToServer(isManual = false) { } // 5. 执行端到端混合信封加密 (RSA-OAEP + AES-256-GCM) + await addExtensionLog('info', '信封加密', '使用 RSA-OAEP + AES-256-GCM 封装高强度密文包') const envelope = await encryptPayloadWithEnvelope(payload, serverPublicKeySpki) // 6. 安全回传至服务端 const syncUrl = `${serverUrl}/api/v1/pdd/extension/sync-cookie` + await addExtensionLog('info', '上传Cookie', `密文已发送至网关: ${syncUrl},等待服务端实测校验...`) + const syncResp = await fetch(syncUrl, { method: 'POST', headers: { @@ -127,10 +316,15 @@ async function syncCookieToServer(isManual = false) { body: JSON.stringify({ envelope }), }) - const syncJson = await syncResp.json() + const syncJson = await syncResp.json().catch(() => null) - if (!syncResp.ok || syncJson.code !== 0) { + if (!syncResp.ok || !syncJson || syncJson.code !== 0) { const errMsg = syncJson?.message || `服务端返回错误 HTTP ${syncResp.status}` + await addExtensionLog('error', '上传失败', `服务端返回校验未通过: ${errMsg}`, { + httpStatus: syncResp.status, + code: syncJson?.code, + detail: syncJson?.data?.detail, + }) throw new Error(errMsg) } @@ -141,13 +335,20 @@ async function syncCookieToServer(isManual = false) { lastStatus: 'ok', lastMessage: syncJson.message || '同步成功并已生效', totalOrders: syncJson.data?.totalOrders, + shopName: syncJson.data?.shopName, }) setExtensionBadge('OK', '#52c41a') // 绿色成功 + await addExtensionLog('success', '同步成功', `Cookie 已通过拼多多网关实测校验并生效 (在售/历史订单数: ${syncJson.data?.totalOrders ?? 0})`, { + shopName: syncJson.data?.shopName, + totalOrders: syncJson.data?.totalOrders, + }) + return { success: true, - message: 'Cookie 同步成功!服务端已完成在线校验并持久化生效', + message: syncJson.message || 'Cookie 同步成功!服务端已完成在线校验并持久化生效', totalOrders: syncJson.data?.totalOrders, + shopName: syncJson.data?.shopName, } } catch (err) { const errMsg = err instanceof Error ? err.message : String(err) @@ -191,6 +392,7 @@ chrome.runtime.onInstalled.addListener(() => { chrome.alarms.create(ALARM_NAME, { periodInMinutes: 30, }) + addExtensionLog('info', '扩展安装/更新', '拼多多 Cookie 保活扩展 v1.1.0 后台服务启动') // 首次运行尝试同步一次 syncCookieToServer(false).catch(() => null) }) @@ -205,15 +407,33 @@ chrome.runtime.onMessage.addListener((req, _sender, sendResponse) => { } if (req.action === 'get_status') { - getPddCookieString().then((ck) => { - chrome.storage.local.get(null).then((data) => { - sendResponse({ - hasCookie: Boolean(ck && ck.length > 20), - cookieLength: ck ? ck.length : 0, - storage: data, - }) + chrome.storage.local.get(null).then(async (data) => { + const details = await getPddCookieDetails(data.manualCookie) + sendResponse({ + hasCookie: Boolean(details.cookieStr && details.cookieStr.length > 20), + cookieLength: details.cookieStr ? details.cookieStr.length : 0, + cookieCount: details.cookieCount, + hasPassId: details.hasPassId, + hasRckk: details.hasRckk, + hasNano: details.hasNano, + storage: data, }) }) return true } + + if (req.action === 'get_logs') { + chrome.storage.local.get(['pdd_extension_logs']).then((res) => { + sendResponse({ logs: Array.isArray(res.pdd_extension_logs) ? res.pdd_extension_logs : [] }) + }) + return true + } + + if (req.action === 'clear_logs') { + chrome.storage.local.set({ pdd_extension_logs: [] }).then(async () => { + await addExtensionLog('info', '清空日志', '用户已在插件控制台清空历史运行日志') + sendResponse({ success: true }) + }) + return true + } }) diff --git a/apps/extensions/pdd-cookie-sync/manifest.json b/apps/extensions/pdd-cookie-sync/manifest.json index a0ec6384..a2a88c21 100644 --- a/apps/extensions/pdd-cookie-sync/manifest.json +++ b/apps/extensions/pdd-cookie-sync/manifest.json @@ -1,14 +1,16 @@ { "manifest_version": 3, "name": "拼多多 Cookie 自动保活同步", - "version": "1.0.0", - "description": "自动提取拼多多商家后台新鲜 Cookie 并通过端到端 RSA+AES-GCM 信封加密安全同步至业务总平台", + "version": "1.1.0", + "description": "自动嗅探拼多多商家后台新鲜 Cookie 并通过端到端 RSA+AES-GCM 信封加密安全同步至业务总平台", "permissions": [ "cookies", "storage", "alarms" ], "host_permissions": [ + "*://*.pinduoduo.com/*", + "*://pinduoduo.com/*", "https://mms.pinduoduo.com/*", "http://*/*", "https://*/*" diff --git a/apps/extensions/pdd-cookie-sync/popup.html b/apps/extensions/pdd-cookie-sync/popup.html index 543606eb..2c7d501d 100644 --- a/apps/extensions/pdd-cookie-sync/popup.html +++ b/apps/extensions/pdd-cookie-sync/popup.html @@ -12,8 +12,8 @@ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; } body { - width: 360px; - padding: 16px; + width: 380px; + padding: 14px; background: #f8fafc; color: #1e293b; font-size: 13px; @@ -22,9 +22,9 @@ display: flex; align-items: center; justify-content: space-between; - padding-bottom: 12px; + padding-bottom: 10px; border-bottom: 1px solid #e2e8f0; - margin-bottom: 14px; + margin-bottom: 12px; } .title { font-size: 15px; @@ -34,6 +34,14 @@ align-items: center; gap: 6px; } + .version-tag { + font-size: 10px; + font-weight: normal; + background: #e0e7ff; + color: #3730a3; + padding: 1px 6px; + border-radius: 10px; + } .badge { display: inline-block; padding: 2px 8px; @@ -43,43 +51,50 @@ } .badge-ok { background: #dcfce7; color: #15803d; } .badge-err { background: #fee2e2; color: #b91c1c; } + .badge-warn { background: #fef3c7; color: #b45309; } .badge-gray { background: #f1f5f9; color: #64748b; } .card { background: #ffffff; border: 1px solid #e2e8f0; border-radius: 8px; - padding: 12px; - margin-bottom: 12px; + padding: 11px; + margin-bottom: 10px; box-shadow: 0 1px 2px rgba(0,0,0,0.03); } .row { display: flex; justify-content: space-between; - margin-bottom: 8px; + align-items: center; + margin-bottom: 7px; } .row:last-child { margin-bottom: 0; } .label { color: #64748b; + font-size: 12px; } .val { font-weight: 500; color: #0f172a; + font-size: 12px; + } + .val-highlight { + color: #2563eb; } .form-group { - margin-bottom: 10px; + margin-bottom: 9px; } .form-group label { display: block; - margin-bottom: 4px; + margin-bottom: 3px; font-weight: 500; font-size: 12px; color: #334155; } input, textarea { width: 100%; - padding: 7px 10px; + padding: 6px 9px; border: 1px solid #cbd5e1; border-radius: 6px; font-size: 12px; @@ -93,7 +108,7 @@ .btn { display: inline-block; width: 100%; - padding: 8px 14px; + padding: 8px 12px; border-radius: 6px; font-size: 13px; font-weight: 500; @@ -113,19 +128,100 @@ background: #ffffff; border: 1px solid #cbd5e1; color: #334155; - margin-top: 6px; } .btn-secondary:hover { background: #f8fafc; border-color: #94a3b8; } + .btn-xs { + width: auto; + padding: 2px 7px; + font-size: 11px; + border-radius: 4px; + background: #f1f5f9; + border: 1px solid #cbd5e1; + color: #475569; + cursor: pointer; + } + .btn-xs:hover { + background: #e2e8f0; + color: #1e293b; + } .btn:disabled { opacity: 0.6; cursor: not-allowed; } + .msg-box { + font-size: 12px; + padding: 8px 10px; + border-radius: 6px; + margin-top: 8px; + display: none; + line-height: 1.4; + } + .msg-success { background: #f0fdf4; color: #166534; border: 1px solid #bbf7d0; } + .msg-error { background: #fef2f2; color: #991b1b; border: 1px solid #fecaca; } + .msg-warn { background: #fffbeb; color: #92400e; border: 1px solid #fde68a; } + + /* 日志控制台样式 */ + .log-panel { + margin-top: 10px; + } + .log-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 6px; + } + .log-title { + font-size: 12px; + font-weight: 600; + color: #475569; + display: flex; + align-items: center; + gap: 6px; + } + .log-actions { + display: flex; + gap: 4px; + } + .log-box { + background: #0f172a; + color: #e2e8f0; + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; + font-size: 11px; + border-radius: 6px; + padding: 8px; + max-height: 150px; + overflow-y: auto; + line-height: 1.5; + } + .log-item { + padding: 2px 0; + border-bottom: 1px dashed #1e293b; + word-break: break-all; + } + .log-item:last-child { + border-bottom: none; + } + .log-time { + color: #64748b; + margin-right: 4px; + } + .log-tag-info { color: #38bdf8; font-weight: 600; margin-right: 4px; } + .log-tag-success { color: #4ade80; font-weight: 600; margin-right: 4px; } + .log-tag-warn { color: #facc15; font-weight: 600; margin-right: 4px; } + .log-tag-error { color: #f87171; font-weight: 600; margin-right: 4px; } + .log-action { color: #a78bfa; margin-right: 4px; } + .log-empty { + color: #64748b; + text-align: center; + padding: 12px 0; + } + .security-tag { font-size: 11px; - color: #64748b; + color: #94a3b8; text-align: center; margin-top: 10px; display: flex; @@ -133,21 +229,13 @@ justify-content: center; gap: 4px; } - .msg-box { - font-size: 11px; - padding: 6px 10px; - border-radius: 4px; - margin-top: 8px; - display: none; - } - .msg-success { background: #f0fdf4; color: #166534; border: 1px solid #bbf7d0; } - .msg-error { background: #fef2f2; color: #991b1b; border: 1px solid #fecaca; }
拼多多 Cookie 保活 + v1.1.0
未检测
@@ -168,7 +256,7 @@
@@ -176,17 +264,34 @@ + +
+
+
+ 📋 运行日志 + (0条) +
+
+ + +
+
+
+
暂无运行日志
+
+
+
- 连接配置 - 一键导入配置 JSON + ⚙️ 连接配置 + 导入配置 JSON
@@ -198,10 +303,17 @@
-
+
+
+ + +
diff --git a/apps/extensions/pdd-cookie-sync/popup.js b/apps/extensions/pdd-cookie-sync/popup.js index b9e4d69e..eb732872 100644 --- a/apps/extensions/pdd-cookie-sync/popup.js +++ b/apps/extensions/pdd-cookie-sync/popup.js @@ -1,5 +1,6 @@ /** * Pinduoduo Cookie Sync Extension Popup Script + * 拼多多 Cookie 自动保活弹窗界面交互与实时日志引擎 */ document.addEventListener('DOMContentLoaded', async () => { @@ -21,6 +22,14 @@ document.addEventListener('DOMContentLoaded', async () => { const inputServerUrl = document.getElementById('inputServerUrl') const inputShopId = document.getElementById('inputShopId') const inputSyncToken = document.getElementById('inputSyncToken') + const inputManualCookie = document.getElementById('inputManualCookie') + + const logBox = document.getElementById('logBox') + const logCountBadge = document.getElementById('logCountBadge') + const btnCopyLogs = document.getElementById('btnCopyLogs') + const btnClearLogs = document.getElementById('btnClearLogs') + + let cachedLogs = [] function formatTime(timestamp) { if (!timestamp) return '未同步' @@ -34,27 +43,94 @@ document.addEventListener('DOMContentLoaded', async () => { return `${m}-${day} ${h}:${min}:${s}` } - function showMsg(text, isError = false) { + function showMsg(text, type = 'success') { statusMsg.style.display = 'block' - statusMsg.className = isError ? 'msg-box msg-error' : 'msg-box msg-success' + if (type === 'error') { + statusMsg.className = 'msg-box msg-error' + } else if (type === 'warn') { + statusMsg.className = 'msg-box msg-warn' + } else { + statusMsg.className = 'msg-box msg-success' + } statusMsg.textContent = text } + // 加载并渲染运行日志 + function refreshLogs() { + chrome.runtime.sendMessage({ action: 'get_logs' }, (res) => { + const logs = Array.isArray(res?.logs) ? res.logs : [] + cachedLogs = logs + logCountBadge.textContent = `(${logs.length}条)` + + if (logs.length === 0) { + logBox.innerHTML = '
暂无运行日志
' + return + } + + let html = '' + for (const item of logs) { + let tagClass = 'log-tag-info' + let tagText = '[INFO]' + if (item.level === 'success') { + tagClass = 'log-tag-success' + tagText = '[OK]' + } else if (item.level === 'warn') { + tagClass = 'log-tag-warn' + tagText = '[WARN]' + } else if (item.level === 'error') { + tagClass = 'log-tag-error' + tagText = '[ERR]' + } + + const detailsHtml = item.details + ? `
↳ ${escapeHtml(item.details)}
` + : '' + + html += ` +
+ ${item.time || ''} + ${tagText} + [${escapeHtml(item.action || '系统')}] + ${escapeHtml(item.message || '')} + ${detailsHtml} +
+ ` + } + logBox.innerHTML = html + }) + } + + function escapeHtml(str) { + if (!str) return '' + return String(str) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + } + function refreshStatusView(statusData) { const storage = statusData?.storage || {} const hasCookie = Boolean(statusData?.hasCookie) + const hasPassId = Boolean(statusData?.hasPassId) + const cookieCount = statusData?.cookieCount || 0 // 1. MMS 登录状态 - if (hasCookie) { - mmsStatus.textContent = `已在线 (长度: ${statusData.cookieLength || 0})` + if (hasPassId) { + mmsStatus.textContent = `已就绪 (包含 PASS_ID,共 ${cookieCount} 项)` mmsStatus.style.color = '#15803d' + } else if (hasCookie) { + mmsStatus.textContent = `未含 PASS_ID (共 ${cookieCount} 项)` + mmsStatus.style.color = '#d97706' } else { - mmsStatus.textContent = '未登录 / 无有效Cookie' + mmsStatus.textContent = '未检测到拼多多登录 Cookie' mmsStatus.style.color = '#b91c1c' } // 2. 店铺标识 - if (storage.shopId) { + if (storage.shopName) { + shopDisplay.textContent = `${storage.shopName} (${storage.shopId || ''})` + } else if (storage.shopId) { shopDisplay.textContent = storage.shopId } else { shopDisplay.textContent = '未配置' @@ -75,21 +151,25 @@ document.addEventListener('DOMContentLoaded', async () => { orderCountRow.style.display = 'none' } - // 5. 顶部角标与错误消息 + // 5. 顶部角标与状态消息 if (!storage.serverUrl || !storage.shopId || !storage.syncToken) { statusBadge.className = 'badge badge-gray' statusBadge.textContent = '待配对' + } else if (!hasPassId && !storage.manualCookie) { + statusBadge.className = 'badge badge-warn' + statusBadge.textContent = '未登录MMS' + showMsg('未检测到拼多多后台核心会话 (PASS_ID)。请在当前浏览器登录 mms.pinduoduo.com 并刷新后台页面', 'warn') } else if (storage.lastStatus === 'ok') { statusBadge.className = 'badge badge-ok' statusBadge.textContent = '运行正常' if (storage.lastMessage) { - showMsg(storage.lastMessage, false) + showMsg(storage.lastMessage, 'success') } } else if (storage.lastStatus === 'error') { statusBadge.className = 'badge badge-err' statusBadge.textContent = '同步异常' if (storage.lastMessage) { - showMsg(storage.lastMessage, true) + showMsg(storage.lastMessage, 'error') } } else { statusBadge.className = 'badge badge-gray' @@ -100,12 +180,14 @@ document.addEventListener('DOMContentLoaded', async () => { inputServerUrl.value = storage.serverUrl || '' inputShopId.value = storage.shopId || '' inputSyncToken.value = storage.syncToken || '' + inputManualCookie.value = storage.manualCookie || '' } - // 初始化加载 + // 初始化加载状态与日志 chrome.runtime.sendMessage({ action: 'get_status' }, (res) => { refreshStatusView(res) }) + refreshLogs() // 切换一键导入框 btnToggleImport.addEventListener('click', (e) => { @@ -127,10 +209,11 @@ document.addEventListener('DOMContentLoaded', async () => { if (parsed.serverUrl) inputServerUrl.value = parsed.serverUrl if (parsed.shopId) inputShopId.value = parsed.shopId if (parsed.syncToken) inputSyncToken.value = parsed.syncToken + if (parsed.cookie) inputManualCookie.value = parsed.cookie importArea.style.display = 'none' - showMsg('配置解析成功,请点击下方【保存配置】生效', false) + showMsg('配置解析成功,请点击下方【保存配置并重连】生效', 'success') } catch { - showMsg('JSON 格式不正确,请确保完整复制后台内容', true) + showMsg('JSON 格式不正确,请确保完整复制管理系统后台弹窗中的配置', 'error') } }) @@ -139,9 +222,10 @@ document.addEventListener('DOMContentLoaded', async () => { const serverUrl = inputServerUrl.value.trim() const shopId = inputShopId.value.trim() const syncToken = inputSyncToken.value.trim() + const manualCookie = inputManualCookie.value.trim() if (!serverUrl || !shopId || !syncToken) { - showMsg('请完整填写服务器地址、店铺标识和配对密钥', true) + showMsg('请完整填写服务器地址、店铺标识和配对密钥', 'error') return } @@ -149,9 +233,10 @@ document.addEventListener('DOMContentLoaded', async () => { serverUrl, shopId, syncToken, + manualCookie, }) - showMsg('配置已保存!正在尝试初次加密同步...', false) + showMsg('配置已保存!正在发起加密上传与网关校验...', 'success') triggerSync() }) @@ -165,17 +250,53 @@ document.addEventListener('DOMContentLoaded', async () => { btnSyncNow.textContent = '立即上传同步 Cookie' if (res && res.success) { - showMsg(res.message || '同步成功!', false) + showMsg(res.message || '同步成功!', 'success') } else { - showMsg(res?.message || '同步失败,请检查网络或配置', true) + showMsg(res?.message || '同步失败,请检查网络或配置', 'error') } - // 刷新界面 + // 刷新界面与日志 chrome.runtime.sendMessage({ action: 'get_status' }, (s) => { refreshStatusView(s) }) + refreshLogs() }) } btnSyncNow.addEventListener('click', triggerSync) + + // 复制日志功能 + btnCopyLogs.addEventListener('click', async () => { + if (!cachedLogs || cachedLogs.length === 0) { + alert('当前没有日志可复制') + return + } + + const textLines = cachedLogs.map((item) => { + const detailStr = item.details ? ` | 详情: ${item.details}` : '' + return `[${item.time}] [${item.level.toUpperCase()}] [${item.action}] ${item.message}${detailStr}` + }) + const fullText = `=== 拼多多保活插件运行日志 (共 ${cachedLogs.length} 条) ===\n` + textLines.join('\n') + + try { + await navigator.clipboard.writeText(fullText) + const origText = btnCopyLogs.textContent + btnCopyLogs.textContent = '已复制!' + setTimeout(() => { + btnCopyLogs.textContent = origText + }, 1500) + } catch (err) { + console.error('复制日志到剪贴板失败:', err) + alert('复制失败,请手动选择复制') + } + }) + + // 清空日志功能 + btnClearLogs.addEventListener('click', () => { + if (confirm('确定要清空扩展的历史运行日志吗?')) { + chrome.runtime.sendMessage({ action: 'clear_logs' }, () => { + refreshLogs() + }) + } + }) }) diff --git a/apps/frontend/src/pages/admin/platform/PddPlatformPanel.tsx b/apps/frontend/src/pages/admin/platform/PddPlatformPanel.tsx index c0f3130a..ddff66d9 100644 --- a/apps/frontend/src/pages/admin/platform/PddPlatformPanel.tsx +++ b/apps/frontend/src/pages/admin/platform/PddPlatformPanel.tsx @@ -1311,8 +1311,15 @@ export function PddPlatformPanel() {
  • 在 Chrome / Edge 浏览器打开:chrome://extensions
  • -
  • 开启右上角【开发者模式】,点击左上角【加载已解压的扩展程序】并选择该目录;
  • -
  • 点击浏览器工具栏扩展图标,点击【导入配置】并粘贴上述配置,保存即完成绑定!
  • +
  • + 开启右上角【开发者模式】,点击左上角【加载已解压的扩展程序】并选择该目录(版本 + v1.1.0); +
  • +
  • 点击浏览器工具栏扩展图标,点击【导入配置 JSON】并粘贴上述配置,点击保存;
  • +
  • + 在当前浏览器中保持登录拼多多商家后台(mms.pinduoduo.com + ),插件将全自动保活;若遇异常可随时在插件面板查看【运行日志】。 +