feat(pdd): 拼多多订单支持 out_sku_sn 匹配与浏览器插件加密保活

- 拼多多进单 Cookie 失效即时拦截与告警修复(支持 43001 会话过期等错误识别)
- 拼多多订单映射优先使用 out_sku_sn(外部商家编码)作为商品规格与工单匹配核心字段
- 新增 Chrome/Edge 浏览器扩展 (Manifest V3),支持端到端 RSA-OAEP + AES-256-GCM 信封加密自动保活
- 后台店铺管理新增插件配对面板、专属密钥管理与一键导入配置 JSON
This commit is contained in:
yml2213
2026-09-14 12:24:48 +08:00
parent 38c0cf1223
commit 9fb27b0ad2
26 changed files with 2090 additions and 22 deletions
+62
View File
@@ -0,0 +1,62 @@
# 拼多多商家后台 Cookie 自动保活扩展 (Chrome / Edge Extension)
本浏览器扩展程序专为解决拼多多商家后台(MMS)会话 Cookie 易过期、人工每日 F12 抓包容易遗忘与导致系统漏单而设计。
---
## 核心工作原理与安全机制
1. **自动嗅探与监听(Silent Capture)**:
- 监听 `mms.pinduoduo.com` 的 Cookie 变更事件;
- 自动在后台提取新鲜的会话凭证(包含 `PASS_ID`、`rckk` 等);
- 内置 SHA-256 指纹比对去重机制,无实质变动时不发起网络请求,绝不浪费带宽与频控。
2. **端到端信封加密(End-to-End Envelope Encryption)**:
- **传输层**:强制走 HTTPS / TLS 1.3 通道;
- **应用层**:使用浏览器原生 Web Crypto API 生成随机 256 位 AES-GCM 对称密钥;
- **非对称公钥包装**:使用服务端的 RSA-2048 公钥(RSA-OAEP SHA-256)加密 AES 密钥;
- **防重放与防篡改**:每个数据包附带 60 秒失效时间戳与加密 Nonce,服务端做幂等校验,AES-GCM Auth Tag 确保任意环节防篡改。
- **无私钥泄漏风险**:扩展仅内置/获取公钥,即使他人获取插件源码也绝对无法解密回传数据包。
3. **双向鉴权与店铺绑定(Pairing Token)**:
- 每个店铺在管理后台均有独立的 `syncToken`;
- 只有持有该配对密钥的插件上报才会被服务端接受。
---
## 安装使用指南(面向客服与运营人员)
### 第一步:安装插件到 Chrome / Edge 浏览器
1. 打开 Chrome 或 Edge 浏览器;
2. 在地址栏输入并前往扩展管理页:
- Chrome 浏览器:`chrome://extensions`
- Edge 浏览器:`edge://extensions`
3. 开启页面右上角的 **【开发者模式】(Developer mode)**;
4. 点击左上角出现的 **【加载已解压的扩展程序】(Load unpacked)** 按钮;
5. 在文件选择窗口中,选中本项目目录下的:
`apps/extensions/pdd-cookie-sync`
6. 确认加载,浏览器右上角扩展栏将出现「拼多多 Cookie 自动保活」图标。
---
### 第二步:配对店铺与配置
1. 打开管理系统后台 -> **【平台配置】** -> **【拼多多平台】**;
2. 找到需要保活的目标店铺,在表格操作列点击 **【插件配对】** 按钮;
3. 弹窗中点击 **【一键复制完整配置】**;
4. 点击浏览器右上角扩展图标,在弹窗中点击 **【一键导入配置 JSON】**,粘贴刚才复制的内容,点击 **【解析并填充】**;
5. 点击 **【保存配置并重连】**;
6. 插件立即发起第一次加密上传与连通性验证。
---
### 第三步:日常使用与状态观察
- **正常运行(角标显示绿色 OK)**:
- 客服日常只需像往常一样在浏览器中登录拼多多商家后台(`mms.pinduoduo.com`);
- 只要商家后台处于登录状态,插件会在后台自动完成 Cookie 刷新与加密上报,服务端同步生效,全程无感。
- **角标显示红色 ERR**:
- 代表配对密钥错误或接口网络受阻,点击插件图标可查看具体错误原因。
- **角标显示黄色 OFF / NO_CK**:
- 代表当前浏览器未登录拼多多商家后台,只需在网页中正常登录即可自动恢复。
@@ -0,0 +1,219 @@
/**
* Pinduoduo Cookie Sync Background Service Worker (Manifest V3)
* 拼多多 Cookie 自动嗅探与加密保活后台服务
*/
try {
importScripts('crypto.js')
} catch (e) {
console.error('导入加密工具失败:', e)
}
const ALARM_NAME = 'pdd_cookie_sync_alarm'
let debounceTimer = null
// 设置扩展图标角标状态
function setExtensionBadge(text, color) {
chrome.action.setBadgeText({ text })
if (color) {
chrome.action.setBadgeBackgroundColor({ color })
}
}
// 获取拼多多全量 Cookie 字符串
async function getPddCookieString() {
try {
const cookies = await chrome.cookies.getAll({ domain: 'pinduoduo.com' })
if (!cookies || cookies.length === 0) {
return ''
}
// 拼接成标准 Cookie 请求头格式
return cookies.map((c) => `${c.name}=${c.value}`).join('; ')
} catch (err) {
console.error('读取拼多多 Cookie 异常:', err)
return ''
}
}
// 核心同步逻辑
async function syncCookieToServer(isManual = false) {
const config = await chrome.storage.local.get([
'serverUrl',
'shopId',
'syncToken',
'lastCookieHash',
'lastSyncAt',
])
const serverUrl = String(config.serverUrl || '').trim().replace(/\/+$/, '')
const shopId = String(config.shopId || '').trim()
const syncToken = String(config.syncToken || '').trim()
if (!serverUrl || !shopId || !syncToken) {
setExtensionBadge('CFG', '#faad14') // 待配置
return {
success: false,
message: '请先打开插件弹窗完成服务器地址与店铺配对密钥配置',
}
}
// 1. 读取当前 Cookie
const cookieStr = await getPddCookieString()
if (!cookieStr) {
setExtensionBadge('OFF', '#8c8c8c') // 未登录
await chrome.storage.local.set({
lastStatus: 'offline',
lastMessage: '未在浏览器中检测到拼多多商家后台登录 Cookie',
})
return {
success: false,
message: '未检测到拼多多登录 Cookie,请先在当前浏览器中登录 mms.pinduoduo.com',
}
}
// 2. 计算 Cookie 哈希并进行指纹去重
const currentHash = await computeSha256(cookieStr)
const now = Date.now()
const lastSyncTime = Number(config.lastSyncAt || 0)
// 若非手动强制触发,且 Cookie 未变动,且距离上次同步不足 1 小时,跳过重复上报
if (!isManual && config.lastCookieHash === currentHash && now - lastSyncTime < 3600_000) {
return {
success: true,
skipped: true,
message: 'Cookie 无实质变动,无需重复上报',
}
}
try {
// 3. 请求服务端公钥
const pkUrl = `${serverUrl}/api/v1/pdd/extension/public-key`
const pkResp = await fetch(pkUrl, {
method: 'GET',
headers: { Accept: 'application/json' },
})
if (!pkResp.ok) {
throw new Error(`获取公钥失败 HTTP ${pkResp.status}`)
}
const pkJson = await pkResp.json()
const serverPublicKeySpki = pkJson?.data?.publicKeySpki
if (!serverPublicKeySpki) {
throw new Error('服务端返回的 RSA 公钥格式异常')
}
// 4. 构建明文载荷(syncToken 在加密包内外双重保护)
const payload = {
shopId,
syncToken,
cookie: cookieStr,
userAgent: navigator.userAgent,
timestamp: Date.now(),
nonce: 'ext_' + Math.random().toString(36).slice(2) + Date.now().toString(36),
}
// 5. 执行端到端混合信封加密 (RSA-OAEP + AES-256-GCM)
const envelope = await encryptPayloadWithEnvelope(payload, serverPublicKeySpki)
// 6. 安全回传至服务端
const syncUrl = `${serverUrl}/api/v1/pdd/extension/sync-cookie`
const syncResp = await fetch(syncUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Shop-Token': syncToken,
},
body: JSON.stringify({ envelope }),
})
const syncJson = await syncResp.json()
if (!syncResp.ok || syncJson.code !== 0) {
const errMsg = syncJson?.message || `服务端返回错误 HTTP ${syncResp.status}`
throw new Error(errMsg)
}
// 同步成功,更新持久化状态
await chrome.storage.local.set({
lastCookieHash: currentHash,
lastSyncAt: now,
lastStatus: 'ok',
lastMessage: syncJson.message || '同步成功并已生效',
totalOrders: syncJson.data?.totalOrders,
})
setExtensionBadge('OK', '#52c41a') // 绿色成功
return {
success: true,
message: 'Cookie 同步成功!服务端已完成在线校验并持久化生效',
totalOrders: syncJson.data?.totalOrders,
}
} catch (err) {
const errMsg = err instanceof Error ? err.message : String(err)
console.error('Cookie 自动同步失败:', errMsg)
await chrome.storage.local.set({
lastStatus: 'error',
lastMessage: errMsg,
})
setExtensionBadge('ERR', '#ff4d4f') // 红色错误
return {
success: false,
message: errMsg,
}
}
}
// 监听拼多多 Cookie 实时变动事件(带防抖延时 3 秒)
chrome.cookies.onChanged.addListener((changeInfo) => {
const domain = changeInfo.cookie?.domain || ''
if (domain.includes('pinduoduo.com')) {
if (debounceTimer) {
clearTimeout(debounceTimer)
}
debounceTimer = setTimeout(() => {
syncCookieToServer(false).catch(() => null)
}, 3000)
}
})
// 定时任务:每隔 30 分钟进行一次轻量巡检与必要保活上报
chrome.alarms.onAlarm.addListener((alarm) => {
if (alarm.name === ALARM_NAME) {
syncCookieToServer(false).catch(() => null)
}
})
// 扩展安装或更新时注册定时闹钟
chrome.runtime.onInstalled.addListener(() => {
chrome.alarms.create(ALARM_NAME, {
periodInMinutes: 30,
})
// 首次运行尝试同步一次
syncCookieToServer(false).catch(() => null)
})
// 响应来自 popup.js 的前端交互消息
chrome.runtime.onMessage.addListener((req, _sender, sendResponse) => {
if (req.action === 'trigger_sync') {
syncCookieToServer(true).then(sendResponse).catch((err) => {
sendResponse({ success: false, message: String(err) })
})
return true // 保持异步通信通道
}
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,
})
})
})
return true
}
})
+127
View File
@@ -0,0 +1,127 @@
/**
* Pinduoduo Extension Web Crypto Envelope Encryption Module
* 基于浏览器原生 Web Crypto API 的端到端信封加密工具库(无任何第三方依赖)
*/
// 将 Base64 字符串转换为 ArrayBuffer
function base64ToArrayBuffer(base64) {
const binaryString = atob(base64.replace(/\s+/g, ''))
const len = binaryString.length
const bytes = new Uint8Array(len)
for (let i = 0; i < len; i++) {
bytes[i] = binaryString.charCodeAt(i)
}
return bytes.buffer
}
// 将 ArrayBuffer / Uint8Array 转换为 Base64 字符串
function arrayBufferToBase64(buffer) {
let binary = ''
const bytes = new Uint8Array(buffer)
const len = bytes.byteLength
for (let i = 0; i < len; i++) {
binary += String.fromCharCode(bytes[i])
}
return btoa(binary)
}
// 导入服务端 RSA-OAEP (SHA-256) 公钥 (SPKI 格式)
async function importRsaPublicKey(spkiBase64) {
const cleanSpki = spkiBase64
.replace(/-----BEGIN PUBLIC KEY-----/, '')
.replace(/-----END PUBLIC KEY-----/, '')
.replace(/\s+/g, '')
const keyBuffer = base64ToArrayBuffer(cleanSpki)
return await crypto.subtle.importKey(
'spki',
keyBuffer,
{
name: 'RSA-OAEP',
hash: 'SHA-256',
},
false,
['encrypt'],
)
}
// 计算文本 SHA-256 哈希(用于 Cookie 指纹比对)
async function computeSha256(text) {
const encoder = new TextEncoder()
const data = encoder.encode(text)
const hashBuffer = await crypto.subtle.digest('SHA-256', data)
const hashArray = Array.from(new Uint8Array(hashBuffer))
return hashArray.map((b) => b.toString(16).padStart(2, '0')).join('')
}
/**
* 执行混合信封加密
* 1. 生成一次性 256 位 AES-GCM 密钥 + 12 字节 IV
* 2. 用 AES-256-GCM 加密明文载荷,分离密文与 128 位认证标签 (Tag)
* 3. 用服务端 RSA 公钥加密 AES 密钥
* 4. 返回标准信封包 { key, iv, tag, cipher }
*/
async function encryptPayloadWithEnvelope(payload, serverPublicKeySpki) {
const cryptoObj = self.crypto || window.crypto
if (!cryptoObj || !cryptoObj.subtle) {
throw new Error('当前浏览器环境不支持 Web Crypto API')
}
// 1. 生成一次性 AES-GCM 对称密钥
const aesKey = await cryptoObj.subtle.generateKey(
{
name: 'AES-GCM',
length: 256,
},
true,
['encrypt'],
)
// 2. 生成 12 字节加密 IV
const iv = cryptoObj.getRandomValues(new Uint8Array(12))
// 3. 使用 AES-256-GCM 加密明文 JSON
const encoder = new TextEncoder()
const plainBytes = encoder.encode(JSON.stringify(payload))
const encryptedBuf = await cryptoObj.subtle.encrypt(
{
name: 'AES-GCM',
iv,
tagLength: 128,
},
aesKey,
plainBytes,
)
// Web Crypto AES-GCM 的输出格式为: [密文 (前 N 字节)] + [认证标签 Tag (后 16 字节)]
const totalLen = encryptedBuf.byteLength
const cipherBytes = encryptedBuf.slice(0, totalLen - 16)
const tagBytes = encryptedBuf.slice(totalLen - 16)
// 4. 导出对称密钥原始字节 (32 字节)
const rawAesKey = await cryptoObj.subtle.exportKey('raw', aesKey)
// 5. 导入服务端 RSA 公钥并加密 AES 密钥
const rsaKey = await importRsaPublicKey(serverPublicKeySpki)
const encKeyBuf = await cryptoObj.subtle.encrypt(
{
name: 'RSA-OAEP',
},
rsaKey,
rawAesKey,
)
return {
key: arrayBufferToBase64(encKeyBuf),
iv: arrayBufferToBase64(iv),
tag: arrayBufferToBase64(tagBytes),
cipher: arrayBufferToBase64(cipherBytes),
}
}
// 导出全局函数供 background.js 与 popup.js 引用
if (typeof self !== 'undefined') {
self.encryptPayloadWithEnvelope = encryptPayloadWithEnvelope
self.computeSha256 = computeSha256
self.importRsaPublicKey = importRsaPublicKey
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 863 B

@@ -0,0 +1,28 @@
{
"manifest_version": 3,
"name": "拼多多 Cookie 自动保活同步",
"version": "1.0.0",
"description": "自动提取拼多多商家后台新鲜 Cookie 并通过端到端 RSA+AES-GCM 信封加密安全同步至业务总平台",
"permissions": [
"cookies",
"storage",
"alarms"
],
"host_permissions": [
"https://mms.pinduoduo.com/*",
"http://*/*",
"https://*/*"
],
"background": {
"service_worker": "background.js"
},
"action": {
"default_popup": "popup.html",
"default_title": "拼多多 Cookie 保活配置"
},
"icons": {
"16": "icon.png",
"48": "icon.png",
"128": "icon.png"
}
}
+216
View File
@@ -0,0 +1,216 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>拼多多 Cookie 自动保活</title>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
}
body {
width: 360px;
padding: 16px;
background: #f8fafc;
color: #1e293b;
font-size: 13px;
}
.header {
display: flex;
align-items: center;
justify-content: space-between;
padding-bottom: 12px;
border-bottom: 1px solid #e2e8f0;
margin-bottom: 14px;
}
.title {
font-size: 15px;
font-weight: 600;
color: #0f172a;
display: flex;
align-items: center;
gap: 6px;
}
.badge {
display: inline-block;
padding: 2px 8px;
border-radius: 12px;
font-size: 11px;
font-weight: 500;
}
.badge-ok { background: #dcfce7; color: #15803d; }
.badge-err { background: #fee2e2; color: #b91c1c; }
.badge-gray { background: #f1f5f9; color: #64748b; }
.card {
background: #ffffff;
border: 1px solid #e2e8f0;
border-radius: 8px;
padding: 12px;
margin-bottom: 12px;
box-shadow: 0 1px 2px rgba(0,0,0,0.03);
}
.row {
display: flex;
justify-content: space-between;
margin-bottom: 8px;
}
.row:last-child {
margin-bottom: 0;
}
.label {
color: #64748b;
}
.val {
font-weight: 500;
color: #0f172a;
}
.form-group {
margin-bottom: 10px;
}
.form-group label {
display: block;
margin-bottom: 4px;
font-weight: 500;
font-size: 12px;
color: #334155;
}
input, textarea {
width: 100%;
padding: 7px 10px;
border: 1px solid #cbd5e1;
border-radius: 6px;
font-size: 12px;
outline: none;
transition: border-color 0.2s;
}
input:focus, textarea:focus {
border-color: #3b82f6;
box-shadow: 0 0 0 2px rgba(59,130,246,0.1);
}
.btn {
display: inline-block;
width: 100%;
padding: 8px 14px;
border-radius: 6px;
font-size: 13px;
font-weight: 500;
text-align: center;
cursor: pointer;
border: none;
transition: all 0.2s;
}
.btn-primary {
background: #2563eb;
color: #ffffff;
}
.btn-primary:hover {
background: #1d4ed8;
}
.btn-secondary {
background: #ffffff;
border: 1px solid #cbd5e1;
color: #334155;
margin-top: 6px;
}
.btn-secondary:hover {
background: #f8fafc;
border-color: #94a3b8;
}
.btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.security-tag {
font-size: 11px;
color: #64748b;
text-align: center;
margin-top: 10px;
display: flex;
align-items: center;
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; }
</style>
</head>
<body>
<div class="header">
<div class="title">
<span>拼多多 Cookie 保活</span>
</div>
<div id="statusBadge" class="badge badge-gray">未检测</div>
</div>
<!-- 状态概览卡片 -->
<div class="card">
<div class="row">
<span class="label">MMS 登录状态:</span>
<span id="mmsStatus" class="val">检测中...</span>
</div>
<div class="row">
<span class="label">绑定店铺:</span>
<span id="shopDisplay" class="val">未配置</span>
</div>
<div class="row">
<span class="label">上次同步:</span>
<span id="lastSyncDisplay" class="val">未同步</span>
</div>
<div class="row" id="orderCountRow" style="display: none;">
<span class="label">在售/历史单:</span>
<span id="orderCountDisplay" class="val" style="color: #2563eb;">0 笔</span>
</div>
<div id="statusMsg" class="msg-box"></div>
</div>
<!-- 手动立即同步按钮 -->
<button id="btnSyncNow" class="btn btn-primary">立即上传同步 Cookie</button>
<!-- 配置折叠区 -->
<div style="margin-top: 12px;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px;">
<span style="font-weight: 600; font-size: 12px; color: #475569;">连接配置</span>
<a href="#" id="btnToggleImport" style="font-size: 11px; color: #2563eb; text-decoration: none;">一键导入配置 JSON</a>
</div>
<!-- 导入 JSON 框(默认隐藏) -->
<div id="importArea" style="display: none; margin-bottom: 10px;">
<textarea id="importJsonInput" rows="3" placeholder="在此粘贴后台复制的完整配置 JSON..."></textarea>
<button id="btnApplyImport" class="btn btn-secondary" style="margin-top: 4px; padding: 5px;">解析并填充</button>
</div>
<div class="card" style="margin-bottom: 8px;">
<div class="form-group">
<label for="inputServerUrl">服务端 API 地址</label>
<input type="text" id="inputServerUrl" placeholder="例如:https://order.domain.com">
</div>
<div class="form-group">
<label for="inputShopId">店铺标识 (Shop ID)</label>
<input type="text" id="inputShopId" placeholder="例如:store_pdd_01">
</div>
<div class="form-group" style="margin-bottom: 0;">
<label for="inputSyncToken">专属配对密钥 (Sync Token)</label>
<input type="password" id="inputSyncToken" placeholder="例如:pdd_tok_xxx">
</div>
</div>
<button id="btnSaveConfig" class="btn btn-secondary">保存配置并重连</button>
</div>
<div class="security-tag">
<span>🛡️ RSA-OAEP + AES-256-GCM 端到端信封加密</span>
</div>
<script src="popup.js"></script>
</body>
</html>
+181
View File
@@ -0,0 +1,181 @@
/**
* Pinduoduo Cookie Sync Extension Popup Script
*/
document.addEventListener('DOMContentLoaded', async () => {
const statusBadge = document.getElementById('statusBadge')
const mmsStatus = document.getElementById('mmsStatus')
const shopDisplay = document.getElementById('shopDisplay')
const lastSyncDisplay = document.getElementById('lastSyncDisplay')
const orderCountRow = document.getElementById('orderCountRow')
const orderCountDisplay = document.getElementById('orderCountDisplay')
const statusMsg = document.getElementById('statusMsg')
const btnSyncNow = document.getElementById('btnSyncNow')
const btnSaveConfig = document.getElementById('btnSaveConfig')
const btnToggleImport = document.getElementById('btnToggleImport')
const btnApplyImport = document.getElementById('btnApplyImport')
const importArea = document.getElementById('importArea')
const importJsonInput = document.getElementById('importJsonInput')
const inputServerUrl = document.getElementById('inputServerUrl')
const inputShopId = document.getElementById('inputShopId')
const inputSyncToken = document.getElementById('inputSyncToken')
function formatTime(timestamp) {
if (!timestamp) return '未同步'
const d = new Date(timestamp)
if (isNaN(d.getTime())) return '未同步'
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}`
}
function showMsg(text, isError = false) {
statusMsg.style.display = 'block'
statusMsg.className = isError ? 'msg-box msg-error' : 'msg-box msg-success'
statusMsg.textContent = text
}
function refreshStatusView(statusData) {
const storage = statusData?.storage || {}
const hasCookie = Boolean(statusData?.hasCookie)
// 1. MMS 登录状态
if (hasCookie) {
mmsStatus.textContent = `已在线 (长度: ${statusData.cookieLength || 0})`
mmsStatus.style.color = '#15803d'
} else {
mmsStatus.textContent = '未登录 / 无有效Cookie'
mmsStatus.style.color = '#b91c1c'
}
// 2. 店铺标识
if (storage.shopId) {
shopDisplay.textContent = storage.shopId
} else {
shopDisplay.textContent = '未配置'
}
// 3. 上次同步
if (storage.lastSyncAt) {
lastSyncDisplay.textContent = formatTime(storage.lastSyncAt)
} else {
lastSyncDisplay.textContent = '从未同步'
}
// 4. 订单数
if (storage.totalOrders !== undefined) {
orderCountRow.style.display = 'flex'
orderCountDisplay.textContent = `${storage.totalOrders} 笔`
} else {
orderCountRow.style.display = 'none'
}
// 5. 顶部角标与错误消息
if (!storage.serverUrl || !storage.shopId || !storage.syncToken) {
statusBadge.className = 'badge badge-gray'
statusBadge.textContent = '待配对'
} else if (storage.lastStatus === 'ok') {
statusBadge.className = 'badge badge-ok'
statusBadge.textContent = '运行正常'
if (storage.lastMessage) {
showMsg(storage.lastMessage, false)
}
} else if (storage.lastStatus === 'error') {
statusBadge.className = 'badge badge-err'
statusBadge.textContent = '同步异常'
if (storage.lastMessage) {
showMsg(storage.lastMessage, true)
}
} else {
statusBadge.className = 'badge badge-gray'
statusBadge.textContent = '待同步'
}
// 填充输入框
inputServerUrl.value = storage.serverUrl || ''
inputShopId.value = storage.shopId || ''
inputSyncToken.value = storage.syncToken || ''
}
// 初始化加载
chrome.runtime.sendMessage({ action: 'get_status' }, (res) => {
refreshStatusView(res)
})
// 切换一键导入框
btnToggleImport.addEventListener('click', (e) => {
e.preventDefault()
if (importArea.style.display === 'none') {
importArea.style.display = 'block'
importJsonInput.focus()
} else {
importArea.style.display = 'none'
}
})
// 执行一键导入解析
btnApplyImport.addEventListener('click', () => {
const raw = importJsonInput.value.trim()
if (!raw) return
try {
const parsed = JSON.parse(raw)
if (parsed.serverUrl) inputServerUrl.value = parsed.serverUrl
if (parsed.shopId) inputShopId.value = parsed.shopId
if (parsed.syncToken) inputSyncToken.value = parsed.syncToken
importArea.style.display = 'none'
showMsg('配置解析成功,请点击下方【保存配置】生效', false)
} catch {
showMsg('JSON 格式不正确,请确保完整复制后台内容', true)
}
})
// 保存配置
btnSaveConfig.addEventListener('click', async () => {
const serverUrl = inputServerUrl.value.trim()
const shopId = inputShopId.value.trim()
const syncToken = inputSyncToken.value.trim()
if (!serverUrl || !shopId || !syncToken) {
showMsg('请完整填写服务器地址、店铺标识和配对密钥', true)
return
}
await chrome.storage.local.set({
serverUrl,
shopId,
syncToken,
})
showMsg('配置已保存!正在尝试初次加密同步...', false)
triggerSync()
})
// 手动触发同步
function triggerSync() {
btnSyncNow.disabled = true
btnSyncNow.textContent = '正在加密上传...'
chrome.runtime.sendMessage({ action: 'trigger_sync' }, (res) => {
btnSyncNow.disabled = false
btnSyncNow.textContent = '立即上传同步 Cookie'
if (res && res.success) {
showMsg(res.message || '同步成功!', false)
} else {
showMsg(res?.message || '同步失败,请检查网络或配置', true)
}
// 刷新界面
chrome.runtime.sendMessage({ action: 'get_status' }, (s) => {
refreshStatusView(s)
})
})
}
btnSyncNow.addEventListener('click', triggerSync)
})