请先在南音工作室系统的“发布管理”中创建任务。
+diff --git a/browser-extension/multi-platform-publisher-1.1.15.zip b/browser-extension/multi-platform-publisher-1.1.16.zip similarity index 58% rename from browser-extension/multi-platform-publisher-1.1.15.zip rename to browser-extension/multi-platform-publisher-1.1.16.zip index 5a37c93..39e4ec6 100644 Binary files a/browser-extension/multi-platform-publisher-1.1.15.zip and b/browser-extension/multi-platform-publisher-1.1.16.zip differ diff --git a/browser-extension/multi-platform-publisher-1.1.17.zip b/browser-extension/multi-platform-publisher-1.1.17.zip new file mode 100644 index 0000000..92b3aa5 Binary files /dev/null and b/browser-extension/multi-platform-publisher-1.1.17.zip differ diff --git a/browser-extension/multi-platform-publisher-1.1.15/README.md b/browser-extension/multi-platform-publisher-1.1.17/README.md similarity index 90% rename from browser-extension/multi-platform-publisher-1.1.15/README.md rename to browser-extension/multi-platform-publisher-1.1.17/README.md index a88a99a..c9b210d 100644 --- a/browser-extension/multi-platform-publisher-1.1.15/README.md +++ b/browser-extension/multi-platform-publisher-1.1.17/README.md @@ -7,12 +7,14 @@ 1. 在 Chrome 地址栏打开 `chrome://extensions/`。 2. 打开右上角“开发者模式”。 3. 点击“加载已解压的扩展程序”。 -4. 选择本目录 `browser-extension/multi-platform-publisher`。 +4. 选择解压后的目录 `browser-extension/multi-platform-publisher-1.1.17`。 5. 登录易代码后台以及需要使用的创作者平台。 6. 重新打开后台“发布管理”页面,确认页面显示“发布扩展已连接”。 修改扩展代码后,需要在扩展管理页点击该扩展的“重新加载”,并刷新后台和平台页面。 +点击浏览器工具栏中的扩展图标会打开右侧“多平台发布助手”。侧边栏展示凭证有效期内各平台的最新发布任务;点击平台名称可定位对应标签页,点击刷新图标会重新打开干净的发布页面并重试填写。已发布成功的任务禁止重试,避免重复发布。 + ## 工作范围 - 后台页面创建带短期凭证的发布任务。 diff --git a/browser-extension/multi-platform-publisher-1.1.15/background/service-worker.js b/browser-extension/multi-platform-publisher-1.1.17/background/service-worker.js similarity index 81% rename from browser-extension/multi-platform-publisher-1.1.15/background/service-worker.js rename to browser-extension/multi-platform-publisher-1.1.17/background/service-worker.js index 10bea31..cf9bc46 100644 --- a/browser-extension/multi-platform-publisher-1.1.15/background/service-worker.js +++ b/browser-extension/multi-platform-publisher-1.1.17/background/service-worker.js @@ -23,13 +23,17 @@ const advancingBatches = new Set() chrome.runtime.onInstalled.addListener(() => { chrome.storage.local.set({ publisherExtensionVersion: VERSION }) + configureSidePanel().catch(() => {}) recoverStoredBatches().catch(() => {}) }) chrome.runtime.onStartup.addListener(() => { + configureSidePanel().catch(() => {}) recoverStoredBatches().catch(() => {}) }) +configureSidePanel().catch(() => {}) + chrome.alarms.onAlarm.addListener(alarm => { if (!alarm || !alarm.name.startsWith(BATCH_WATCHDOG_PREFIX)) return const batchId = alarm.name.slice(BATCH_WATCHDOG_PREFIX.length) @@ -79,6 +83,27 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { return true } + if (message.type === 'SIDEPANEL_GET_TASKS') { + listSidePanelTasks() + .then(tasks => sendResponse({ ok: true, tasks })) + .catch(error => sendResponse({ ok: false, message: readableError(error) })) + return true + } + + if (message.type === 'SIDEPANEL_RETRY_TASK') { + retryTask(message.taskId) + .then(result => sendResponse({ ok: true, ...result })) + .catch(error => sendResponse({ ok: false, message: readableError(error) })) + return true + } + + if (message.type === 'SIDEPANEL_ACTIVATE_TASK') { + activateTaskTab(message.taskId) + .then(result => sendResponse({ ok: true, ...result })) + .catch(error => sendResponse({ ok: false, message: readableError(error) })) + return true + } + return false }) @@ -97,10 +122,93 @@ chrome.tabs.onRemoved.addListener(tabId => { await updateStatus(context, 'CANCELLED', '平台页面已关闭', '') await advanceBatch(context) } - await chrome.storage.local.remove([tabKey(tabId), taskKey(context.taskId)]) + context.platformTabId = null + context.closedAt = Date.now() + await chrome.storage.local.set({ [taskKey(context.taskId)]: context }) + await chrome.storage.local.remove(tabKey(tabId)) }).catch(() => {}) }) +async function configureSidePanel() { + if (!chrome.sidePanel || !chrome.sidePanel.setPanelBehavior) return + await chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true }) +} + +async function listSidePanelTasks() { + const stored = await chrome.storage.local.get(null) + const expiry = Date.now() - 4 * 60 * 60 * 1000 + const contexts = Object.keys(stored) + .filter(key => key.startsWith('publisher_task_')) + .map(key => stored[key]) + .filter(context => context && context.taskId && context.platform && context.createdAt >= expiry) + .sort((left, right) => (right.createdAt || 0) - (left.createdAt || 0)) + const latestByPlatform = new Map() + for (const context of contexts) { + if (!latestByPlatform.has(context.platform)) latestByPlatform.set(context.platform, context) + } + return Array.from(latestByPlatform.values()).map(context => ({ + taskId: context.taskId, + platform: context.platform, + platformName: platformName(context.platform), + title: context.task && context.task.title || '', + status: context.status || 'PENDING', + statusMessage: context.statusMessage || '', + platformTabId: context.platformTabId || null, + createdAt: context.createdAt || 0, + canRetry: context.status !== 'SUCCEEDED' + })) +} + +async function activateTaskTab(taskId) { + const context = await requireStoredTask(taskId) + if (!context.platformTabId) throw new Error('平台页面已关闭,可点击刷新按钮重新打开') + const tab = await chrome.tabs.get(context.platformTabId).catch(() => null) + if (!tab) throw new Error('平台页面已关闭,可点击刷新按钮重新打开') + await focusTab(tab) + return { taskId: context.taskId, tabId: tab.id } +} + +async function retryTask(taskId) { + const context = await requireStoredTask(taskId) + if (context.status === 'SUCCEEDED') throw new Error('已发布成功的任务不能重试,以免重复发布') + context.status = 'OPENING' + context.statusMessage = '正在从侧边栏重试' + context.closedAt = null + await updateStatus(context, 'OPENING', '正在重试' + platformName(context.platform) + '发布任务', '') + + let tab = context.platformTabId + ? await chrome.tabs.get(context.platformTabId).catch(() => null) + : null + if (tab) { + await chrome.storage.local.set({ [tabKey(tab.id)]: context.taskId }) + tab = await chrome.tabs.update(tab.id, { url: PLATFORM_URLS[context.platform], active: true }) + } else { + tab = await chrome.tabs.create({ url: PLATFORM_URLS[context.platform], active: true }) + } + context.platformTabId = tab.id + await chrome.storage.local.set({ + [taskKey(context.taskId)]: context, + [tabKey(tab.id)]: context.taskId + }) + await focusTab(tab) + return { taskId: context.taskId, tabId: tab.id } +} + +async function requireStoredTask(taskId) { + if (typeof taskId !== 'string' || !/^publish_[a-f0-9]{32}$/.test(taskId)) { + throw new Error('发布任务编号无效') + } + const stored = await chrome.storage.local.get(taskKey(taskId)) + const context = stored[taskKey(taskId)] + if (!context) throw new Error('发布任务已过期,请从后台重新创建') + return context +} + +async function focusTab(tab) { + if (tab.windowId) await chrome.windows.update(tab.windowId, { focused: true }).catch(() => {}) + await chrome.tabs.update(tab.id, { active: true }) +} + async function startPublish(payload, sender) { if (!isAllowedAdmin(sender.url) || !sender.tab) { throw new Error('发布请求不是来自允许的后台页面') @@ -442,6 +550,8 @@ async function updateStatus(context, status, message, error) { throw new Error(body.msg || '发布状态回写失败') } context.status = status + context.statusMessage = message || '' + context.lastError = error || '' if (context.platformTabId) { await chrome.storage.local.set({ [taskKey(context.taskId)]: context }) } diff --git a/browser-extension/multi-platform-publisher-1.1.15/content/admin-bridge.js b/browser-extension/multi-platform-publisher-1.1.17/content/admin-bridge.js similarity index 100% rename from browser-extension/multi-platform-publisher-1.1.15/content/admin-bridge.js rename to browser-extension/multi-platform-publisher-1.1.17/content/admin-bridge.js diff --git a/browser-extension/multi-platform-publisher-1.1.15/content/bilibili.js b/browser-extension/multi-platform-publisher-1.1.17/content/bilibili.js similarity index 100% rename from browser-extension/multi-platform-publisher-1.1.15/content/bilibili.js rename to browser-extension/multi-platform-publisher-1.1.17/content/bilibili.js diff --git a/browser-extension/multi-platform-publisher-1.1.15/content/douyin.js b/browser-extension/multi-platform-publisher-1.1.17/content/douyin.js similarity index 100% rename from browser-extension/multi-platform-publisher-1.1.15/content/douyin.js rename to browser-extension/multi-platform-publisher-1.1.17/content/douyin.js diff --git a/browser-extension/multi-platform-publisher-1.1.15/content/platform-common.js b/browser-extension/multi-platform-publisher-1.1.17/content/platform-common.js similarity index 94% rename from browser-extension/multi-platform-publisher-1.1.15/content/platform-common.js rename to browser-extension/multi-platform-publisher-1.1.17/content/platform-common.js index c387613..13406f7 100644 --- a/browser-extension/multi-platform-publisher-1.1.15/content/platform-common.js +++ b/browser-extension/multi-platform-publisher-1.1.17/content/platform-common.js @@ -151,8 +151,11 @@ const blob = await response.blob() if (!blob.size) throw new Error(assetName(kind) + '文件为空') const disposition = response.headers.get('Content-Disposition') || '' - const fileName = parseFileName(disposition) || fallbackName - return new File([blob], fileName, { type: blob.type || mimeForName(fileName), lastModified: Date.now() }) + const sourceName = parseFileName(disposition) || fallbackName + const mimeType = normalizedAssetMimeType(blob.type, sourceName) + validateAssetMimeType(kind, mimeType) + const fileName = fileNameForMimeType(sourceName, mimeType) + return new File([blob], fileName, { type: mimeType, lastModified: Date.now() }) } async function activatePublishMode(config, task) { @@ -808,6 +811,45 @@ }[extension] || 'application/octet-stream' } + function normalizedAssetMimeType(responseType, fileName) { + const normalized = String(responseType || '').split(';')[0].trim().toLowerCase() + return normalized && normalized !== 'application/octet-stream' + ? normalized + : mimeForName(fileName) + } + + function validateAssetMimeType(kind, mimeType) { + if ((kind === 'image' || kind === 'cover') + && !['image/png', 'image/jpeg', 'image/webp'].includes(mimeType)) { + throw new Error(assetName(kind) + '返回的不是支持的图片格式(' + mimeType + ')') + } + if (kind === 'video' && ![ + 'video/mp4', 'video/quicktime', 'video/x-m4v', 'video/webm' + ].includes(mimeType)) { + throw new Error('视频返回的不是支持的视频格式(' + mimeType + ')') + } + } + + function fileNameForMimeType(fileName, mimeType) { + const expectedExtension = { + 'image/png': 'png', + 'image/jpeg': 'jpg', + 'image/webp': 'webp', + 'video/mp4': 'mp4', + 'video/quicktime': 'mov', + 'video/x-m4v': 'm4v', + 'video/webm': 'webm' + }[mimeType] + const normalizedName = String(fileName || 'publish-asset').trim() || 'publish-asset' + if (!expectedExtension) return normalizedName + const currentMimeType = mimeForName(normalizedName) + if (currentMimeType === mimeType) return normalizedName + const lastSlash = Math.max(normalizedName.lastIndexOf('/'), normalizedName.lastIndexOf('\\')) + const lastDot = normalizedName.lastIndexOf('.') + const baseName = lastDot > lastSlash ? normalizedName.slice(0, lastDot) : normalizedName + return baseName + '.' + expectedExtension + } + function assetName(kind) { return { video: '视频', image: '图片', cover: '封面' }[kind] || '素材' } @@ -903,5 +945,13 @@ function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)) } function readableError(error) { return error && error.message ? error.message : String(error || '未知错误') } - globalThis.YidaimaPublisher = Object.freeze({ run, watch }) + const publisherApi = { run, watch } + if (globalThis.__YIDAIMA_PUBLISHER_TEST__) { + publisherApi.test = Object.freeze({ + normalizedAssetMimeType, + validateAssetMimeType, + fileNameForMimeType + }) + } + globalThis.YidaimaPublisher = Object.freeze(publisherApi) })() diff --git a/browser-extension/multi-platform-publisher-1.1.15/content/wechat-channels.js b/browser-extension/multi-platform-publisher-1.1.17/content/wechat-channels.js similarity index 100% rename from browser-extension/multi-platform-publisher-1.1.15/content/wechat-channels.js rename to browser-extension/multi-platform-publisher-1.1.17/content/wechat-channels.js diff --git a/browser-extension/multi-platform-publisher-1.1.15/content/xianyu.js b/browser-extension/multi-platform-publisher-1.1.17/content/xianyu.js similarity index 100% rename from browser-extension/multi-platform-publisher-1.1.15/content/xianyu.js rename to browser-extension/multi-platform-publisher-1.1.17/content/xianyu.js diff --git a/browser-extension/multi-platform-publisher-1.1.15/content/xiaohongshu.js b/browser-extension/multi-platform-publisher-1.1.17/content/xiaohongshu.js similarity index 100% rename from browser-extension/multi-platform-publisher-1.1.15/content/xiaohongshu.js rename to browser-extension/multi-platform-publisher-1.1.17/content/xiaohongshu.js diff --git a/browser-extension/multi-platform-publisher-1.1.15/manifest.json b/browser-extension/multi-platform-publisher-1.1.17/manifest.json similarity index 91% rename from browser-extension/multi-platform-publisher-1.1.15/manifest.json rename to browser-extension/multi-platform-publisher-1.1.17/manifest.json index 5524458..ad372e8 100644 --- a/browser-extension/multi-platform-publisher-1.1.15/manifest.json +++ b/browser-extension/multi-platform-publisher-1.1.17/manifest.json @@ -1,13 +1,14 @@ { "manifest_version": 3, "name": "易代码多平台发布助手", - "version": "1.1.15", + "version": "1.1.17", "description": "从易代码后台读取发布任务,为小红书、抖音、视频号、B站和闲鱼填写发布资料,最终发布由用户手动确认。", "permissions": [ "storage", "tabs", "scripting", - "alarms" + "alarms", + "sidePanel" ], "host_permissions": [ "https://feast.yidaima.cn/*", @@ -24,8 +25,10 @@ "service_worker": "background/service-worker.js" }, "action": { - "default_title": "易代码多平台发布助手", - "default_popup": "popup/popup.html" + "default_title": "打开多平台发布助手" + }, + "side_panel": { + "default_path": "sidepanel/panel.html" }, "content_scripts": [ { diff --git a/browser-extension/multi-platform-publisher-1.1.15/popup/popup.html b/browser-extension/multi-platform-publisher-1.1.17/popup/popup.html similarity index 100% rename from browser-extension/multi-platform-publisher-1.1.15/popup/popup.html rename to browser-extension/multi-platform-publisher-1.1.17/popup/popup.html diff --git a/browser-extension/multi-platform-publisher-1.1.15/popup/popup.js b/browser-extension/multi-platform-publisher-1.1.17/popup/popup.js similarity index 100% rename from browser-extension/multi-platform-publisher-1.1.15/popup/popup.js rename to browser-extension/multi-platform-publisher-1.1.17/popup/popup.js diff --git a/browser-extension/multi-platform-publisher-1.1.17/sidepanel/panel.css b/browser-extension/multi-platform-publisher-1.1.17/sidepanel/panel.css new file mode 100644 index 0000000..b3f649e --- /dev/null +++ b/browser-extension/multi-platform-publisher-1.1.17/sidepanel/panel.css @@ -0,0 +1,151 @@ +:root { + color-scheme: light; + font-family: Arial, "Microsoft YaHei", sans-serif; + color: #273142; + background: #f6f8fb; +} + +* { box-sizing: border-box; } +body { min-width: 300px; margin: 0; background: #f6f8fb; } +button { font: inherit; } + +.panel-header { + display: flex; + align-items: flex-start; + justify-content: space-between; + padding: 18px 16px 14px; + background: #fff; + border-bottom: 1px solid #e7ebf1; +} +.panel-header h1 { margin: 0; font-size: 17px; line-height: 24px; } +.panel-header p { margin: 6px 0 0; color: #7a8495; font-size: 12px; line-height: 1.5; } +.header-mark { + display: inline-grid; + width: 20px; + height: 20px; + margin-right: 4px; + place-items: center; + color: #fff; + background: #28b66f; + border-radius: 5px; +} +.header-action, +.retry-button { + border: 0; + cursor: pointer; + transition: background .15s ease, color .15s ease, transform .15s ease; +} +.header-action { + width: 32px; + height: 32px; + color: #657084; + background: #f1f4f8; + border-radius: 8px; + font-size: 19px; +} +.header-action:hover { color: #1677ff; background: #eaf3ff; } + +main { padding: 12px 12px 54px; } +.notice { + margin-bottom: 10px; + padding: 9px 11px; + color: #9b5b00; + background: #fff7e8; + border: 1px solid #ffe0a3; + border-radius: 8px; + font-size: 12px; + line-height: 1.5; +} +.notice.is-success { color: #237849; background: #edf9f2; border-color: #bce8cd; } +.task-list { display: grid; gap: 9px; } +.task-card { + display: flex; + align-items: center; + min-height: 66px; + background: #fff; + border: 1px solid #e5e9f0; + border-radius: 10px; + box-shadow: 0 2px 8px rgba(35, 47, 70, .04); + overflow: hidden; +} +.task-main { + display: flex; + flex: 1; + align-items: center; + min-width: 0; + padding: 11px 8px 11px 12px; + text-align: left; + background: transparent; + border: 0; + cursor: pointer; +} +.task-main:hover { background: #fafcff; } +.platform-icon { + display: inline-grid; + flex: 0 0 34px; + width: 34px; + height: 34px; + margin-right: 10px; + place-items: center; + color: #fff; + border-radius: 9px; + font-size: 14px; + font-weight: 700; +} +.platform-xiaohongshu { background: #ff2442; } +.platform-douyin { background: #15171d; } +.platform-wechat_channels { background: #19b875; } +.platform-bilibili { background: #00aeec; } +.platform-xianyu { color: #322d00; background: #ffe500; } +.task-copy { display: flex; flex: 1; min-width: 0; flex-direction: column; } +.platform-name { color: #273142; font-size: 14px; line-height: 20px; } +.task-title { + margin-top: 2px; + overflow: hidden; + color: #8a93a3; + font-size: 11px; + line-height: 16px; + text-overflow: ellipsis; + white-space: nowrap; +} +.status-line { display: flex; align-items: center; margin-top: 4px; color: #697386; font-size: 11px; } +.status-line i { width: 6px; height: 6px; margin-right: 5px; background: #9aa3b2; border-radius: 50%; } +.status-line.is-running i { background: #1677ff; box-shadow: 0 0 0 3px #e8f2ff; } +.status-line.is-ready i { background: #28b66f; box-shadow: 0 0 0 3px #e8f8ef; } +.status-line.is-failed i { background: #f04438; box-shadow: 0 0 0 3px #ffedeb; } +.status-line.is-login i { background: #f59f00; box-shadow: 0 0 0 3px #fff3d6; } +.retry-button { + flex: 0 0 42px; + width: 42px; + height: 42px; + margin-right: 8px; + color: #667085; + background: #f5f7fa; + border-radius: 9px; + font-size: 21px; +} +.retry-button:hover { color: #1677ff; background: #eaf3ff; transform: rotate(-20deg); } +.retry-button:disabled { color: #b7bec9; background: #f5f6f8; cursor: not-allowed; transform: none; } +.retry-button.is-loading { animation: spin .8s linear infinite; } +@keyframes spin { to { transform: rotate(360deg); } } + +.empty-state { padding: 54px 24px; color: #7b8595; text-align: center; } +.empty-state .empty-icon { margin-bottom: 12px; color: #a9b2c0; font-size: 34px; } +.empty-state strong { display: block; color: #4b5567; font-size: 14px; } +.empty-state p { margin: 8px 0 0; font-size: 12px; line-height: 1.6; } + +footer { + position: fixed; + right: 0; + bottom: 0; + left: 0; + display: flex; + align-items: center; + padding: 10px 14px; + color: #7f8998; + background: rgba(255, 255, 255, .96); + border-top: 1px solid #e7ebf1; + font-size: 11px; +} +.connection-dot { width: 7px; height: 7px; margin-right: 6px; background: #28b66f; border-radius: 50%; } +.version { margin-left: auto; } diff --git a/browser-extension/multi-platform-publisher-1.1.17/sidepanel/panel.html b/browser-extension/multi-platform-publisher-1.1.17/sidepanel/panel.html new file mode 100644 index 0000000..ba5ddd0 --- /dev/null +++ b/browser-extension/multi-platform-publisher-1.1.17/sidepanel/panel.html @@ -0,0 +1,50 @@ + + +
+ + +点击平台名称定位页面,点击刷新图标重新填写
+请先在南音工作室系统的“发布管理”中创建任务。
+