1041 lines
45 KiB
JavaScript
1041 lines
45 KiB
JavaScript
/**
|
||
* 千帆发布页 DOM 自动化脚本 v2
|
||
* 注入到 https://ark.xiaohongshu.com/* 页面
|
||
*
|
||
* 选择器基于实际千帆页面 DOM 结构(2026-07-18 实测确认)。
|
||
* 千帆发布页是多步表单:先上传图片,再填写内容并发布。
|
||
*/
|
||
|
||
// ==================== 选择器配置(集中管理,便于修改)====================
|
||
const SELECTORS = {
|
||
// --- 上传步骤 ---
|
||
// 图片文件上传 input(multiple, accept=.jpg,.jpeg,.png,.webp)
|
||
imageUploadInput: 'input.upload-input[type="file"][multiple]',
|
||
// 上传按钮
|
||
imageUploadButton: 'button.upload-button',
|
||
// 上传区域(用于拖拽检测)
|
||
imageDropZone: '.upload-wrapper',
|
||
// 上传进度/图片列表容器
|
||
imagePreviewList: '.upload-list, .image-list, [class*="preview"]',
|
||
|
||
// --- 内容编辑步骤(上传图片后出现)---
|
||
// 正文编辑器(contenteditable div)
|
||
contentEditor: '[contenteditable="true"]',
|
||
// 备选:Quill / ProseMirror 编辑器
|
||
contentEditorAlt: '.ql-editor, .ProseMirror, [class*="editor"] [contenteditable]',
|
||
// 标题输入(如有独立标题输入框)
|
||
titleInput: 'input[placeholder*="标题"], input[class*="title"]',
|
||
// 话题输入框
|
||
topicInput: 'input[placeholder*="话题"], input[placeholder*="标签"], input[class*="topic"]',
|
||
// 话题建议下拉项
|
||
topicSuggestion: '.topic-suggestion, [class*="topic-list"] > div, .suggest-item, .d-dropdown-item',
|
||
|
||
// --- 选品步骤 ---
|
||
productSearchInput: '#ark-app-note-mount-container > div > div > div > div.main-item-list-wrap > div.list-header > div.list-header-right > div > div > input',
|
||
// 搜索结果列表容器("去发笔记" 链接在其中,按文本匹配而非固定 nth-child,避免结果位置变化导致失效)
|
||
productListContainer: '#ark-app-note-mount-container .main-item-list-wrap .list-content',
|
||
|
||
// --- 发布页:实测确认的精确选择器(2026-07-20)---
|
||
// "上传图文" tab
|
||
imageTextTab: '#ark > div.outarea.upload-c > div > div > div.header > div:nth-child(2) > span',
|
||
// 标题输入框
|
||
titleInputExact: '#ark > div.outarea.publish-c > div > div > div > div.body > div.content > div.input.titleInput > div.d-input-wrapper.d-inline-block.c-input_inner > div > input',
|
||
// 正文编辑器(Quill,容器 id 为 quillEditor)
|
||
contentEditorExact: '#quillEditor > div',
|
||
// 发布按钮
|
||
publishButtonExact: '#ark > div.outarea.publish-c > div > div > div > div.submit > div > button.publishBtn',
|
||
// 发布成功页的"继续发布/返回"按钮
|
||
publishSuccessContinueButton: '#ark-app-note-mount-container > div > div > div.d-result > div > div.d-result-extra > button.d-button.d-button-default.d-button-with-content.--color-static.bold.--color-bg-primary.--color-white',
|
||
|
||
// --- 页面状态 ---
|
||
// 主发布容器
|
||
mainContainer: '.note-publish-wrap',
|
||
// 上传步骤容器
|
||
uploadStep: '.upload-content',
|
||
// 内容编辑步骤容器
|
||
editStep: '.content-editor, [class*="edit-content"], [class*="note-content"]',
|
||
};
|
||
|
||
// ==================== 从 storage 加载自定义选择器 ====================
|
||
|
||
async function loadCustomSelectors() {
|
||
return new Promise((resolve) => {
|
||
chrome.storage.local.get(["config"], (data) => {
|
||
const cfg = data.config || {};
|
||
const custom = {};
|
||
if (cfg.selSearchInput) custom.productSearchInput = cfg.selSearchInput;
|
||
if (cfg.selUploadArea) custom.imageUploadInput = cfg.selUploadArea;
|
||
if (cfg.selEditor) custom.contentEditor = cfg.selEditor;
|
||
if (cfg.selTopicInput) custom.topicInput = cfg.selTopicInput;
|
||
if (cfg.selSubmitBtn) custom.publishButton = cfg.selSubmitBtn;
|
||
resolve(custom);
|
||
});
|
||
});
|
||
}
|
||
|
||
// ==================== 工具函数 ====================
|
||
|
||
function sleep(ms) {
|
||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||
}
|
||
|
||
// 等待元素出现
|
||
function waitForElement(selector, timeout = 10000) {
|
||
return new Promise((resolve, reject) => {
|
||
const existing = document.querySelector(selector);
|
||
if (existing) return resolve(existing);
|
||
|
||
const observer = new MutationObserver(() => {
|
||
const el = document.querySelector(selector);
|
||
if (el) {
|
||
observer.disconnect();
|
||
resolve(el);
|
||
}
|
||
});
|
||
observer.observe(document.body, {childList: true, subtree: true});
|
||
|
||
setTimeout(() => {
|
||
observer.disconnect();
|
||
reject(new Error(`等待元素超时: ${selector}`));
|
||
}, timeout);
|
||
});
|
||
}
|
||
|
||
// 尝试多个选择器,返回第一个匹配的
|
||
function queryFirst(selectors) {
|
||
for (const sel of selectors) {
|
||
try {
|
||
const el = document.querySelector(sel);
|
||
if (el) return el;
|
||
} catch (e) { /* 忽略无效选择器 */
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
function getEditableElement(element) {
|
||
if (!element) return null;
|
||
if (element.tagName === "INPUT" || element.tagName === "TEXTAREA" || element.contentEditable === "true") return element;
|
||
return element.querySelector('input, textarea, [contenteditable="true"]') || element;
|
||
}
|
||
|
||
// 模拟人类输入(触发 React/Vue 受控组件的事件)
|
||
function simulateInput(element, value) {
|
||
element = getEditableElement(element);
|
||
element.focus();
|
||
element.dispatchEvent(new Event("focus", {bubbles: true}));
|
||
|
||
if (element.tagName === "INPUT" || element.tagName === "TEXTAREA") {
|
||
element.value = "";
|
||
element.dispatchEvent(new Event("input", {bubbles: true}));
|
||
|
||
const proto = element.tagName === "TEXTAREA"
|
||
? window.HTMLTextAreaElement.prototype
|
||
: window.HTMLInputElement.prototype;
|
||
const nativeSetter = Object.getOwnPropertyDescriptor(proto, "value")?.set;
|
||
if (nativeSetter) {
|
||
nativeSetter.call(element, value);
|
||
} else {
|
||
element.value = value;
|
||
}
|
||
element.dispatchEvent(new Event("input", {bubbles: true}));
|
||
element.dispatchEvent(new Event("change", {bubbles: true}));
|
||
} else if (element.contentEditable === "true") {
|
||
// 方法1: 使用 execCommand(兼容性最好)
|
||
element.innerHTML = "";
|
||
document.execCommand("selectAll", false, null);
|
||
document.execCommand("delete", false, null);
|
||
|
||
// 将内容按段落分割,逐段写入
|
||
const paragraphs = value.split(/\n/);
|
||
for (let i = 0; i < paragraphs.length; i++) {
|
||
if (i > 0) {
|
||
document.execCommand("insertParagraph", false, null);
|
||
}
|
||
if (paragraphs[i]) {
|
||
document.execCommand("insertText", false, paragraphs[i]);
|
||
}
|
||
}
|
||
|
||
// 触发事件同步框架状态
|
||
element.dispatchEvent(new InputEvent("input", {bubbles: true, inputType: "insertText"}));
|
||
element.dispatchEvent(new Event("change", {bubbles: true}));
|
||
|
||
// 方法2(备选): 如果 execCommand 不生效,使用 clipboard paste
|
||
if (!element.textContent.trim() && value.trim()) {
|
||
const clipboardData = new DataTransfer();
|
||
clipboardData.setData("text/plain", value);
|
||
const pasteEvent = new ClipboardEvent("paste", {
|
||
bubbles: true,
|
||
cancelable: true,
|
||
clipboardData: clipboardData,
|
||
});
|
||
element.dispatchEvent(pasteEvent);
|
||
}
|
||
}
|
||
|
||
element.dispatchEvent(new Event("blur", {bubbles: true}));
|
||
}
|
||
|
||
// 获取配置
|
||
async function getConfig() {
|
||
return new Promise((resolve) => {
|
||
chrome.storage.local.get(["config"], (data) => {
|
||
resolve(data.config || {serverUrl: "http://localhost:3000"});
|
||
});
|
||
});
|
||
}
|
||
|
||
// 通过 background.js 代理获取图片(绕过 CORS 和混合内容限制)
|
||
function fetchImageViaBackground(imageUrl) {
|
||
return new Promise((resolve, reject) => {
|
||
chrome.runtime.sendMessage({type: "FETCH_IMAGE", url: imageUrl}, (res) => {
|
||
if (chrome.runtime.lastError) {
|
||
reject(new Error(chrome.runtime.lastError.message));
|
||
return;
|
||
}
|
||
if (res && res.ok) {
|
||
resolve(res.dataUrl);
|
||
} else {
|
||
reject(new Error(res && res.error ? res.error : "图片获取失败"));
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
// 模拟文件上传(通过 DataTransfer 注入 file input)
|
||
async function uploadFilesToFileInput(fileInput, imageUrls) {
|
||
const files = [];
|
||
|
||
for (const url of imageUrls) {
|
||
try {
|
||
console.log("[千帆助手] 正在获取图片:", url);
|
||
const dataUrl = await fetchImageViaBackground(url);
|
||
const imgResp = await fetch(dataUrl);
|
||
const blob = await imgResp.blob();
|
||
const filename = url.split("/").pop() || `image_${Date.now()}.jpg`;
|
||
files.push(new File([blob], filename, {type: blob.type || "image/jpeg"}));
|
||
} catch (e) {
|
||
console.error("[千帆助手] 图片下载失败:", url, e);
|
||
}
|
||
}
|
||
|
||
if (files.length === 0) throw new Error("没有可用的图片文件");
|
||
|
||
const dataTransfer = new DataTransfer();
|
||
files.forEach((f) => dataTransfer.items.add(f));
|
||
|
||
fileInput.files = dataTransfer.files;
|
||
fileInput.dispatchEvent(new Event("change", {bubbles: true}));
|
||
fileInput.dispatchEvent(new Event("input", {bubbles: true}));
|
||
|
||
return files.length;
|
||
}
|
||
|
||
// 模拟拖拽上传(备选方案)
|
||
async function simulateDropUpload(dropZone, imageUrls) {
|
||
const files = [];
|
||
for (const url of imageUrls) {
|
||
try {
|
||
console.log("[千帆助手] 正在获取图片:", url);
|
||
const dataUrl = await fetchImageViaBackground(url);
|
||
const imgResp = await fetch(dataUrl);
|
||
const blob = await imgResp.blob();
|
||
files.push(new File([blob], url.split("/").pop() || `image_${Date.now()}.jpg`, {type: blob.type || "image/jpeg"}));
|
||
} catch (e) {
|
||
console.error("[千帆助手] 图片下载失败:", url, e);
|
||
}
|
||
}
|
||
if (files.length === 0) throw new Error("没有可用的图片文件");
|
||
|
||
const dataTransfer = new DataTransfer();
|
||
files.forEach((f) => dataTransfer.items.add(f));
|
||
|
||
dropZone.dispatchEvent(new DragEvent("dragenter", {bubbles: true, dataTransfer}));
|
||
dropZone.dispatchEvent(new DragEvent("dragover", {bubbles: true, dataTransfer}));
|
||
dropZone.dispatchEvent(new DragEvent("drop", {bubbles: true, dataTransfer}));
|
||
|
||
return files.length;
|
||
}
|
||
|
||
// 逐字符向 Quill/富文本编辑器插入文本(CJK 友好)
|
||
// 合成 keydown 不携带中文,故用 execCommand insertText 逐字插入触发 Quill text-change
|
||
async function typeIntoQuill(editor, text) {
|
||
editor.focus();
|
||
editor.click();
|
||
for (const ch of text) {
|
||
if (ch === "\n") {
|
||
editor.dispatchEvent(new KeyboardEvent("keydown", {
|
||
key: "Enter",
|
||
code: "Enter",
|
||
keyCode: 13,
|
||
bubbles: true
|
||
}));
|
||
document.execCommand("insertParagraph", false, null);
|
||
editor.dispatchEvent(new KeyboardEvent("keyup", {key: "Enter", code: "Enter", keyCode: 13, bubbles: true}));
|
||
} else {
|
||
editor.dispatchEvent(new KeyboardEvent("keydown", {key: ch, bubbles: true}));
|
||
editor.dispatchEvent(new KeyboardEvent("keypress", {key: ch, bubbles: true}));
|
||
document.execCommand("insertText", false, ch);
|
||
editor.dispatchEvent(new InputEvent("input", {bubbles: true, inputType: "insertText", data: ch}));
|
||
editor.dispatchEvent(new KeyboardEvent("keyup", {key: ch, bubbles: true}));
|
||
}
|
||
}
|
||
editor.dispatchEvent(new Event("change", {bubbles: true}));
|
||
editor.dispatchEvent(new Event("blur", {bubbles: true}));
|
||
}
|
||
|
||
// 在 Quill 编辑器触发 # mention:插入 #+话题名 → 等 mention 下拉 → 选第一项
|
||
// 返回 true 表示 mention 选中;false 表示下拉未出现,#话题名已作为纯文本留在正文里
|
||
async function clickQuillMention(editor, topic) {
|
||
editor.focus();
|
||
const trigger = (ch) => {
|
||
editor.dispatchEvent(new KeyboardEvent("keydown", {key: ch, bubbles: true}));
|
||
document.execCommand("insertText", false, ch);
|
||
editor.dispatchEvent(new InputEvent("input", {bubbles: true, inputType: "insertText", data: ch}));
|
||
editor.dispatchEvent(new KeyboardEvent("keyup", {key: ch, bubbles: true}));
|
||
};
|
||
trigger("#");
|
||
for (const ch of topic) trigger(ch);
|
||
|
||
let list;
|
||
try {
|
||
list = await waitForElement("div.ql-mention-list-container", 5000);
|
||
} catch (e) {
|
||
return false;
|
||
}
|
||
const firstItem = list.querySelector(".ql-mention-list li, .ql-mention-item, .d-dropdown-item, li");
|
||
if (!firstItem) return false;
|
||
firstItem.click();
|
||
return true;
|
||
}
|
||
|
||
// 按文本查找按钮(精确匹配优先,避免误匹配)
|
||
function findButtonByText(text) {
|
||
const buttons = document.querySelectorAll("button");
|
||
// First pass: exact match
|
||
for (const btn of buttons) {
|
||
const btnText = btn.textContent.trim();
|
||
if (btnText === text && !btn.disabled) {
|
||
return btn;
|
||
}
|
||
}
|
||
// Second pass: contains match (exclude buttons with different primary text)
|
||
const excludeWords = {"发布": ["存草稿"]};
|
||
for (const btn of buttons) {
|
||
const btnText = btn.textContent.trim();
|
||
if (btnText.includes(text) && !btn.disabled) {
|
||
const excludes = excludeWords[text] || [];
|
||
if (!excludes.some(ex => btnText.includes(ex))) {
|
||
return btn;
|
||
}
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
// Wait for page to transition away from item-select to publish page
|
||
function waitForPublishPage(timeoutMs = 20000) {
|
||
return new Promise((resolve, reject) => {
|
||
const startTime = Date.now();
|
||
|
||
function check() {
|
||
// Check if we're now on the publish page (not item-select)
|
||
if (isPublishPage()) {
|
||
return resolve(true);
|
||
}
|
||
// Also check if URL changed away from itemSelect
|
||
if (!window.location.href.includes("itemSelect") &&
|
||
!window.location.href.includes("publish-item-select")) {
|
||
return resolve(true);
|
||
}
|
||
if (Date.now() - startTime > timeoutMs) {
|
||
return reject(new Error("等待发布页超时,请确认已选择商品"));
|
||
}
|
||
setTimeout(check, 500);
|
||
}
|
||
|
||
check();
|
||
});
|
||
}
|
||
|
||
async function waitForPageReady(timeoutMs = 30000) {
|
||
if (document.readyState === "complete") return;
|
||
|
||
await new Promise((resolve) => {
|
||
const done = () => {
|
||
window.removeEventListener("load", done);
|
||
resolve();
|
||
};
|
||
window.addEventListener("load", done, {once: true});
|
||
setTimeout(done, timeoutMs);
|
||
});
|
||
}
|
||
|
||
async function waitForAnyElement(selectors, timeoutMs = 15000) {
|
||
const startedAt = Date.now();
|
||
while (Date.now() - startedAt < timeoutMs) {
|
||
const el = queryFirst(selectors);
|
||
if (el) return el;
|
||
await sleep(Math.floor(Math.random() * 2001) + 1000);
|
||
}
|
||
return null;
|
||
}
|
||
|
||
function clickElement(el) {
|
||
el.scrollIntoView?.({behavior: "smooth", block: "center", inline: "center"});
|
||
el.dispatchEvent(new MouseEvent("mouseover", {bubbles: true}));
|
||
el.dispatchEvent(new MouseEvent("mousedown", {bubbles: true, button: 0}));
|
||
el.dispatchEvent(new MouseEvent("mouseup", {bubbles: true, button: 0}));
|
||
el.dispatchEvent(new MouseEvent("click", {bubbles: true, button: 0}));
|
||
}
|
||
|
||
function findClickableByText(selector, text) {
|
||
return Array.from(document.querySelectorAll(selector)).find((el) => el.textContent.trim().includes(text));
|
||
}
|
||
|
||
// ==================== 核心发布流程 ====================
|
||
|
||
// Wait for publish result after clicking publish button
|
||
// 之前用 "!url.includes('publish')" 来判断"已经不在发布页=成功",
|
||
// 但发布成功页 URL 是 .../app-note/publish-success,本身就带 "publish",这个判断从来没生效过,
|
||
// 导致每次都要等满 15 秒超时才"假设成功",而且从来没有真正处理过成功页。
|
||
function waitForPublishResult(timeoutMs = 30000) {
|
||
return new Promise((resolve) => {
|
||
const startTime = Date.now();
|
||
|
||
function check() {
|
||
if (isPublishSuccessPage()) {
|
||
return resolve({success: true, onSuccessPage: true});
|
||
}
|
||
|
||
const successToast = document.querySelector(
|
||
'.toast-success, [class*="success-toast"], [class*="message-success"], .el-message--success'
|
||
);
|
||
if (successToast) return resolve({success: true, onSuccessPage: false});
|
||
|
||
const errorToast = document.querySelector(
|
||
'.toast-error, [class*="error-toast"], [class*="message-error"], .el-message--error'
|
||
);
|
||
if (errorToast) return resolve({success: false, error: errorToast.textContent.trim() || "发布失败"});
|
||
|
||
if (Date.now() - startTime > timeoutMs) {
|
||
console.warn("[千帆助手] 发布结果检测超时,假设成功");
|
||
return resolve({success: true, onSuccessPage: false, timedOut: true});
|
||
}
|
||
|
||
setTimeout(check, 500);
|
||
}
|
||
|
||
check();
|
||
});
|
||
}
|
||
|
||
// 在"笔记发布成功"页点击继续按钮,并等待页面跳回选品/搜索页,供下一条笔记继续发布
|
||
async function continueFromPublishSuccessPage(timeoutMs = 15000) {
|
||
console.log("[千帆助手] 已到达发布成功页,准备点击继续按钮");
|
||
|
||
// 先等结果区容器渲染出来,再去找按钮,避免页面刚跳转、DOM 还没渲染完就查询
|
||
await waitForAnyElement(["#ark-app-note-mount-container .d-result", "#ark-app-note-mount-container"], 8000);
|
||
await sleep(Math.floor(Math.random() * 2001) + 1000);
|
||
|
||
let continueBtn = await waitForAnyElement([
|
||
SELECTORS.publishSuccessContinueButton,
|
||
"#ark-app-note-mount-container .d-result-extra button",
|
||
".d-result-extra button",
|
||
".d-result button",
|
||
], 12000);
|
||
|
||
if (!continueBtn) {
|
||
// 再按文字兜底找一次(比如"继续发布"、"再发一篇"、"返回"之类的文案)
|
||
continueBtn = findClickableByText("button, a", "继续")
|
||
|| findClickableByText("button, a", "再发")
|
||
|| findClickableByText("button, a", "返回")
|
||
|| findClickableByText("button, a", "下一");
|
||
}
|
||
|
||
if (!continueBtn) {
|
||
alertSelectorFailure("publish_success_continue", SELECTORS.publishSuccessContinueButton,
|
||
"continue button not found on publish-success page",
|
||
[SELECTORS.publishSuccessContinueButton, ".d-result-extra button", ".d-result button"]);
|
||
console.warn("[千帆助手] 未找到发布成功页的继续按钮,流程可能会停在此页");
|
||
return false;
|
||
}
|
||
|
||
clickElement(continueBtn);
|
||
console.log("[千帆助手] 已点击继续按钮,等待跳转到选品页");
|
||
|
||
const startTime = Date.now();
|
||
while (Date.now() - startTime < timeoutMs) {
|
||
if (isItemSelectPage() || !isPublishSuccessPage()) {
|
||
console.log("[千帆助手] 已跳回选品页,可继续下一条笔记");
|
||
return true;
|
||
}
|
||
await sleep(Math.floor(Math.random() * 2001) + 1000);
|
||
}
|
||
console.warn("[千帆助手] 点击继续按钮后,等待跳转超时,仍停留在发布成功页");
|
||
return false;
|
||
}
|
||
|
||
async function waitForImageUploadComplete(expectedCount, timeoutMs = 20000) {
|
||
const startTime = Date.now();
|
||
let lastLog = 0;
|
||
|
||
while (Date.now() - startTime < timeoutMs) {
|
||
const uploading = document.querySelector(
|
||
'.upload-loading, [class*="uploading"], [class*="upload-progress"], .el-loading, [class*="loading"]'
|
||
);
|
||
const previewCount = document.querySelectorAll(
|
||
'[class*="preview"] img, [class*="upload-list"] img, [class*="image-item"] img, [class*="upload"] img'
|
||
).length;
|
||
const errorEl = document.querySelector('[class*="upload-error"], [class*="upload-fail"]');
|
||
|
||
if (errorEl) {
|
||
console.warn("[千帆助手] 检测到图片上传失败标志:", errorEl.textContent.trim().slice(0, 50));
|
||
}
|
||
|
||
const elapsed = Date.now() - startTime;
|
||
if (elapsed - lastLog > 5000) {
|
||
console.log(`[千帆助手] 等待图片上传中... 已等待 ${Math.round(elapsed / 1000)}s,预览图数量 ${previewCount}/${expectedCount},上传中标志: ${!!uploading}`);
|
||
lastLog = elapsed;
|
||
}
|
||
|
||
// 没有"上传中"标志,且预览图数量已经达到预期,认为上传完成
|
||
if (!uploading && previewCount >= expectedCount) {
|
||
console.log(`[千帆助手] 图片上传完成,预览图数量 ${previewCount}/${expectedCount}`);
|
||
await sleep(Math.floor(Math.random() * 2001) + 1000);
|
||
return true;
|
||
}
|
||
|
||
await sleep(Math.floor(Math.random() * 2001) + 1000);
|
||
}
|
||
|
||
console.warn("[千帆助手] 等待图片上传完成超时,继续往下走(可能图片实际还没传完)");
|
||
return false;
|
||
}
|
||
|
||
// 更可靠的逐字符输入:比一次性用 nativeSetter 塞入整个字符串更接近真实打字,
|
||
// 部分带防抖/搜索联想的输入框,一次性 setter 赋值容易被内部逻辑吞掉或截断
|
||
// (尤其是中文,如果组件依赖 IME composition 事件判断"输入是否结束")。
|
||
async function typeTextRobust(inputEl, text) {
|
||
const proto = inputEl.tagName === "TEXTAREA"
|
||
? window.HTMLTextAreaElement.prototype
|
||
: window.HTMLInputElement.prototype;
|
||
const nativeSetter = Object.getOwnPropertyDescriptor(proto, "value")?.set;
|
||
|
||
inputEl.focus();
|
||
inputEl.dispatchEvent(new Event("focus", {bubbles: true}));
|
||
|
||
// 先清空
|
||
if (nativeSetter) nativeSetter.call(inputEl, ""); else inputEl.value = "";
|
||
inputEl.dispatchEvent(new InputEvent("input", {bubbles: true, inputType: "deleteContentBackward"}));
|
||
|
||
// 模拟中文输入法:compositionstart → 逐字 compositionupdate → compositionend
|
||
inputEl.dispatchEvent(new CompositionEvent("compositionstart", {bubbles: true, data: ""}));
|
||
|
||
let current = "";
|
||
for (const ch of text) {
|
||
current += ch;
|
||
inputEl.dispatchEvent(new KeyboardEvent("keydown", {key: ch, bubbles: true}));
|
||
if (nativeSetter) nativeSetter.call(inputEl, current); else inputEl.value = current;
|
||
inputEl.dispatchEvent(new CompositionEvent("compositionupdate", {bubbles: true, data: current}));
|
||
inputEl.dispatchEvent(new InputEvent("input", {bubbles: true, inputType: "insertText", data: ch}));
|
||
inputEl.dispatchEvent(new KeyboardEvent("keyup", {key: ch, bubbles: true}));
|
||
}
|
||
|
||
inputEl.dispatchEvent(new CompositionEvent("compositionend", {bubbles: true, data: current}));
|
||
// compositionend 后很多框架会重新按最终值触发一次 input,保险起见再补一次
|
||
if (nativeSetter) nativeSetter.call(inputEl, current); else inputEl.value = current;
|
||
inputEl.dispatchEvent(new InputEvent("input", {bubbles: true, inputType: "insertFromComposition", data: current}));
|
||
inputEl.dispatchEvent(new Event("change", {bubbles: true}));
|
||
|
||
return current;
|
||
}
|
||
|
||
// 在结果列表里找"标题包含 searchTerm"的商品卡片,再定位卡片内的"去发笔记"链接,
|
||
// 而不是不加区分地拿容器里第一个"去发笔记"链接——如果搜索没真正生效(列表还是默认
|
||
// 全部商品),那样每次点的都是排在最前面的默认商品,而不是搜索到的目标商品。
|
||
function findGoNoteLinkForProduct(container, searchTerm) {
|
||
if (!container) return null;
|
||
const candidateLinks = Array.from(container.querySelectorAll("a, button"))
|
||
.filter((el) => el.textContent.trim().includes("去发笔记"));
|
||
for (const link of candidateLinks) {
|
||
let node = link;
|
||
for (let depth = 0; depth < 8 && node; depth++, node = node.parentElement) {
|
||
if (node.textContent && node.textContent.includes(searchTerm)) {
|
||
return link;
|
||
}
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
async function executePublish(task) {
|
||
const note = task.note;
|
||
if (!note) throw new Error("任务中没有笔记数据");
|
||
|
||
console.log(`[千帆助手] 开始发布: ${note.title}`);
|
||
|
||
const customSelectors = await loadCustomSelectors();
|
||
const S = {...SELECTORS, ...customSelectors};
|
||
|
||
await waitForPageReady();
|
||
|
||
// --- 选品页:搜索商品并跳转 ---
|
||
if (isItemSelectPage()) {
|
||
// C1: 搜索词 = product_title 按 | 切第一段(与 Automa 实测一致)
|
||
const rawTitle = note.product_title || note.title || "";
|
||
const searchTerm = rawTitle.split("|")[0].trim();
|
||
if (!searchTerm) throw new Error("笔记缺少商品标题,无法在选品页搜索");
|
||
console.log("[千帆助手] STEP 1: 选品页搜索商品", {searchTerm, fullTitle: rawTitle});
|
||
|
||
const productSearchSelectors = [
|
||
S.productSearchInput,
|
||
"#ark-app-note-mount-container .main-item-list-wrap .list-header .list-header-right input",
|
||
"#ark-app-note-mount-container .main-item-list-wrap .list-header .list-header-right > div > div",
|
||
".main-item-list-wrap .list-header-right input",
|
||
".main-item-list-wrap .list-header-right [contenteditable='true']",
|
||
".pressing > .d-text",
|
||
"input.d-text",
|
||
"[class*='d-text']",
|
||
'input[placeholder*="商品"]',
|
||
'input[placeholder*="搜索"]',
|
||
];
|
||
const searchBox = await waitForAnyElement(productSearchSelectors, 15000);
|
||
if (!searchBox) {
|
||
alertSelectorFailure("search_input", S.productSearchInput, "product search box not found", productSearchSelectors);
|
||
throw new Error("未找到商品搜索框");
|
||
}
|
||
|
||
// 很多搜索框是"先点一下才展开出真正 <input>"的交互;如果点击前就把外层 div 当成
|
||
// 可编辑元素锁定住,simulateInput 对一个既不是 input/textarea 也不是 contenteditable
|
||
// 的普通 div 什么都不会做——搜索词根本没输进去,Enter 自然也无效。
|
||
// 所以这里先点击容器,等一下,再重新查找真正的可编辑元素。
|
||
clickElement(searchBox);
|
||
await sleep(Math.floor(Math.random() * 2001) + 1000);
|
||
|
||
let searchInput = getEditableElement(
|
||
queryFirst(productSearchSelectors) || searchBox
|
||
);
|
||
// 点击后再等一次,给组件时间渲染出真正的 input
|
||
if (!searchInput || (searchInput.tagName !== "INPUT" && searchInput.tagName !== "TEXTAREA" && searchInput.contentEditable !== "true")) {
|
||
await sleep(Math.floor(Math.random() * 2001) + 1000);
|
||
searchInput = getEditableElement(queryFirst(productSearchSelectors) || searchBox);
|
||
}
|
||
|
||
const isRealEditable = !!searchInput && (
|
||
searchInput.tagName === "INPUT" || searchInput.tagName === "TEXTAREA" || searchInput.contentEditable === "true"
|
||
);
|
||
|
||
console.log("[千帆助手] STEP 1.5: 搜索框元素", {
|
||
tag: searchInput && searchInput.tagName,
|
||
isRealEditable,
|
||
cls: searchInput && searchInput.className,
|
||
});
|
||
|
||
// B2: 输入搜索词(逐字符输入 + 输入后校验,避免一次性赋值被吞导致标题不完整)
|
||
if (isRealEditable) {
|
||
clickElement(searchInput);
|
||
const typedValue = await typeTextRobust(searchInput, searchTerm);
|
||
await sleep(Math.floor(Math.random() * 2001) + 1000);
|
||
if (searchInput.value !== searchTerm) {
|
||
console.warn("[千帆助手] 搜索框内容与预期不符,重试一次", {
|
||
expected: searchTerm, actual: searchInput.value, typedValue,
|
||
});
|
||
await sleep(Math.floor(Math.random() * 2001) + 1000);
|
||
await typeTextRobust(searchInput, searchTerm);
|
||
await sleep(Math.floor(Math.random() * 2001) + 1000);
|
||
if (searchInput.value !== searchTerm) {
|
||
console.warn("[千帆助手] 重试后搜索框内容仍不完整,继续尝试搜索", {
|
||
expected: searchTerm, actual: searchInput.value,
|
||
});
|
||
}
|
||
}
|
||
} else {
|
||
// 兜底:找不到真正的 input/contenteditable,直接对着容器逐字符派发键盘事件
|
||
// (部分自定义组件靠原生键盘事件驱动,不依赖 value/input 事件)
|
||
console.warn("[千帆助手] 未找到真正的可编辑输入框,改用逐字符键盘事件兜底输入");
|
||
searchInput = searchBox;
|
||
clickElement(searchInput);
|
||
for (const ch of searchTerm) {
|
||
searchInput.dispatchEvent(new KeyboardEvent("keydown", {key: ch, bubbles: true}));
|
||
searchInput.dispatchEvent(new KeyboardEvent("keypress", {key: ch, bubbles: true}));
|
||
searchInput.dispatchEvent(new InputEvent("input", {bubbles: true, inputType: "insertText", data: ch}));
|
||
searchInput.dispatchEvent(new KeyboardEvent("keyup", {key: ch, bubbles: true}));
|
||
}
|
||
}
|
||
|
||
console.log("[千帆助手] 搜索框当前内容:", searchInput.value ?? searchInput.textContent);
|
||
await sleep(Math.floor(Math.random() * 2001) + 1000);
|
||
searchInput.dispatchEvent(new Event("change", {bubbles: true}));
|
||
searchInput.dispatchEvent(new KeyboardEvent("keydown", {
|
||
key: "Enter",
|
||
code: "Enter",
|
||
keyCode: 13,
|
||
which: 13,
|
||
bubbles: true
|
||
}));
|
||
searchInput.dispatchEvent(new KeyboardEvent("keypress", {
|
||
key: "Enter",
|
||
code: "Enter",
|
||
keyCode: 13,
|
||
which: 13,
|
||
bubbles: true
|
||
}));
|
||
searchInput.dispatchEvent(new KeyboardEvent("keyup", {
|
||
key: "Enter",
|
||
code: "Enter",
|
||
keyCode: 13,
|
||
which: 13,
|
||
bubbles: true
|
||
}));
|
||
await sleep(Math.floor(Math.random() * 2001) + 1000);
|
||
|
||
|
||
// B3: 校验搜索是否真的生效 —— 结果列表里要能找到"标题包含 searchTerm"的商品卡片。
|
||
// 找不到的话,先尝试点搜索按钮/图标兜底,再多等几秒重试,而不是直接退而求其次点默认第一个。
|
||
const listContainer = document.querySelector(S.productListContainer);
|
||
let targetLink = findGoNoteLinkForProduct(listContainer, searchTerm);
|
||
|
||
if (!targetLink) {
|
||
const searchBtn = queryFirst([
|
||
".list-header-right button",
|
||
".list-header-right [class*='search-icon']",
|
||
".list-header-right [class*='searchBtn']",
|
||
".list-header-right svg",
|
||
]);
|
||
if (searchBtn) {
|
||
console.log("[千帆助手] Enter 未生效或搜索未命中,尝试点击搜索按钮/图标兜底");
|
||
clickElement(searchBtn);
|
||
await sleep(Math.floor(Math.random() * 2001) + 1000);
|
||
targetLink = findGoNoteLinkForProduct(listContainer, searchTerm);
|
||
}
|
||
}
|
||
|
||
// 再多等几次,给搜索请求返回结果留时间(最多再等 5 秒)
|
||
for (let i = 0; i < 5 && !targetLink; i++) {
|
||
await sleep(Math.floor(Math.random() * 2001) + 1000);
|
||
targetLink = findGoNoteLinkForProduct(document.querySelector(S.productListContainer), searchTerm);
|
||
}
|
||
|
||
if (!targetLink) {
|
||
// 最后兜底:只要页面上任意位置能匹配到"标题含 searchTerm 的卡片 + 去发笔记链接"就用,
|
||
// 但绝不退化为"随便点容器里第一个去发笔记"(那样等于点了默认第一条,选错商品)。
|
||
targetLink = findGoNoteLinkForProduct(document.body, searchTerm);
|
||
}
|
||
|
||
if (!targetLink) {
|
||
alertSelectorFailure("product_link", "a:去发笔记",
|
||
`搜索"${searchTerm}"后,未在结果列表中找到匹配该商品标题的"去发笔记"链接(很可能是搜索没有真正生效,列表仍是默认全部商品)`,
|
||
[S.productListContainer, "a.d-link", "a:去发笔记", "button:去发笔记"]);
|
||
throw new Error(`未找到 ${searchTerm} 的"去发笔记"链接,商品可能不存在,或搜索未生效`);
|
||
}
|
||
|
||
console.log("[千帆助手] STEP 2: 找到去发笔记链接,点击");
|
||
clickElement(targetLink);
|
||
|
||
try {
|
||
await waitForPublishPage(20000);
|
||
console.log("[千帆助手] STEP 3: 已进入发布页");
|
||
} catch (e) {
|
||
throw new Error("点击去发笔记后未能跳转到发布页: " + e.message);
|
||
}
|
||
}
|
||
|
||
// --- 发布页 ---
|
||
if (!isPublishPage()) {
|
||
console.warn("[千帆助手] 无法确认当前在发布页,仍尝试继续");
|
||
}
|
||
|
||
// B3.5: 等待发布页主容器渲染完成(URL 变化不等于 DOM 渲染完成,之前 STEP4 常常在容器还没挂载时就查询,误判"未找到")
|
||
console.log("[千帆助手] STEP 3.5: 等待发布页容器渲染");
|
||
await waitForAnyElement(["#ark .outarea.upload-c", "#ark .outarea", "#ark"], 10000);
|
||
await sleep(Math.floor(Math.random() * 2001) + 1000);
|
||
|
||
// B4: 点击"上传图文" tab(用轮询等待,而不是一次性查询)
|
||
console.log("[千帆助手] STEP 4: 点击上传图文 tab");
|
||
const imageTextTab = await waitForAnyElement([
|
||
S.imageTextTab,
|
||
'.outarea.upload-c .header div:nth-child(2) span',
|
||
'.outarea.upload-c .header div:nth-child(2)',
|
||
".creator-tab:nth-child(2) > .title",
|
||
".creator-tab:nth-child(2)",
|
||
'[class*="creator-tab"]:nth-child(2)',
|
||
], 8000);
|
||
if (imageTextTab) {
|
||
clickElement(imageTextTab);
|
||
await sleep(Math.floor(Math.random() * 2001) + 1000);
|
||
} else {
|
||
console.warn("[千帆助手] 未找到上传图文 tab,可能已在图文模式,也可能选择器已过期");
|
||
alertSelectorFailure("image_text_tab", S.imageTextTab, "image-text tab not found",
|
||
[S.imageTextTab, '.outarea.upload-c .header div:nth-child(2) span', ".creator-tab:nth-child(2)"]);
|
||
}
|
||
|
||
// B5: 上传图片
|
||
if (note.image_paths && note.image_paths.length > 0) {
|
||
console.log(`[千帆助手] STEP 5: 上传 ${note.image_paths.length} 张图片`);
|
||
try {
|
||
const fileInput = await waitForAnyElement([
|
||
S.imageUploadInput,
|
||
'input[type="file"][multiple]',
|
||
"input.upload-input",
|
||
'input[type="file"]',
|
||
], 8000);
|
||
if (fileInput) {
|
||
await uploadFilesToFileInput(fileInput, note.image_paths);
|
||
console.log("[千帆助手] STEP 5: 图片已注入 file input");
|
||
} else {
|
||
const dropZone = queryFirst([S.imageDropZone, ".upload-wrapper", ".upload-content"]);
|
||
if (dropZone) {
|
||
await simulateDropUpload(dropZone, note.image_paths);
|
||
console.log("[千帆助手] STEP 5: 图片已通过拖拽注入");
|
||
} else {
|
||
alertSelectorFailure("image_upload", S.imageUploadInput, "image upload area not found",
|
||
[S.imageUploadInput, 'input[type="file"][multiple]', "input.upload-input", ".upload-wrapper"]);
|
||
throw new Error("未找到图片上传区域");
|
||
}
|
||
}
|
||
// 等待上传完成:图片上传通常比较慢,之前只等最多 20 秒,很容易在图片还没传完时
|
||
// 就往下走,导致后面填标题/正文时其实图片还没上传成功。
|
||
// 这里改成:最多等 90 秒,每秒检查一次"上传中"标志是否消失 + 已生成的图片预览数量,
|
||
// 并每 5 秒打印一次进度日志,方便在控制台里看到它到底卡在哪。
|
||
await waitForImageUploadComplete(note.image_paths.length, 20000);
|
||
} catch (e) {
|
||
throw new Error(`图片上传失败: ${e.message}`);
|
||
}
|
||
}
|
||
|
||
// B6: 填标题(发布页标题字段 .focus > .d-text,区别于选品页搜索框)
|
||
if (note.title) {
|
||
console.log("[千帆助手] STEP 6: 填写标题", {title: note.title});
|
||
let titleField = null;
|
||
for (let i = 0; i < 10; i++) {
|
||
titleField = queryFirst([
|
||
S.titleInputExact,
|
||
'.outarea.publish-c .titleInput input',
|
||
".focus > .d-text",
|
||
'input[placeholder*="标题"]',
|
||
'input[class*="title"]',
|
||
"input.d-text",
|
||
]);
|
||
if (titleField) break;
|
||
await sleep(Math.floor(Math.random() * 2001) + 1000);
|
||
|
||
}
|
||
if (titleField) {
|
||
try {
|
||
titleField.click();
|
||
} catch (_) {
|
||
}
|
||
simulateInput(titleField, note.title);
|
||
titleField.dispatchEvent(new Event("change", {bubbles: true}));
|
||
await sleep(Math.floor(Math.random() * 2001) + 1000);
|
||
console.log("[千帆助手] STEP 6: 标题已填写");
|
||
} else {
|
||
alertSelectorFailure("title_field", ".focus > .d-text", "title field not found",
|
||
[".focus > .d-text", 'input[placeholder*="标题"]', 'input[class*="title"]', "input.d-text"]);
|
||
}
|
||
}
|
||
|
||
// B7: 定位正文编辑器(Quill div.ql-editor)
|
||
console.log("[千帆助手] STEP 7: 定位正文编辑器");
|
||
let editor = null;
|
||
for (let i = 0; i < 10; i++) {
|
||
editor = queryFirst([
|
||
S.contentEditorExact,
|
||
"#quillEditor .ql-editor",
|
||
"#quillEditor > div",
|
||
S.contentEditor,
|
||
S.contentEditorAlt,
|
||
"div.ql-editor",
|
||
'[contenteditable="true"]',
|
||
".ProseMirror",
|
||
]);
|
||
if (editor) break;
|
||
await sleep(Math.floor(Math.random() * 2001) + 1000);
|
||
}
|
||
if (!editor) {
|
||
alertSelectorFailure("content_editor", "div.ql-editor", "editor not found",
|
||
[S.contentEditor, S.contentEditorAlt, "div.ql-editor", '[contenteditable="true"]', ".ProseMirror"]);
|
||
throw new Error("未找到正文编辑器");
|
||
}
|
||
try {
|
||
editor.focus();
|
||
editor.click();
|
||
} catch (_) {
|
||
}
|
||
await sleep(Math.floor(Math.random() * 2001) + 1000);
|
||
|
||
// B8: 填正文(typeIntoQuill 逐字 execCommand,CJK 友好;失败降级 simulateInput)
|
||
if (note.content) {
|
||
console.log(`[千帆助手] STEP 8: 填写正文 (${note.content.length} 字)`);
|
||
try {
|
||
await typeIntoQuill(editor, note.content);
|
||
} catch (e) {
|
||
console.warn("[千帆助手] typeIntoQuill 失败,降级 simulateInput:", e.message);
|
||
simulateInput(editor, note.content);
|
||
}
|
||
await sleep(Math.floor(Math.random() * 2001) + 1000);
|
||
}
|
||
|
||
// B9: 追加热搜词(直接接在正文后面的纯文本,不走 # mention 下拉选择流程)
|
||
if (note.topics && note.topics.length > 0) {
|
||
console.log("[千帆助手] STEP 9: 追加热搜词", {topics: note.topics});
|
||
const hotWords = note.topics.slice(0, 5).join(" ");
|
||
try {
|
||
await typeIntoQuill(editor, " " + hotWords);
|
||
} catch (e) {
|
||
console.warn("[千帆助手] typeIntoQuill 追加热搜词失败,降级 simulateInput:", e.message);
|
||
simulateInput(editor, (note.content || "") + " " + hotWords);
|
||
}
|
||
await sleep(Math.floor(Math.random() * 2001) + 1000);
|
||
}
|
||
|
||
// B10: 滚动到发布区,确保发布按钮在视口内可点击
|
||
console.log("[千帆助手] STEP 10: 滚动到发布区");
|
||
const publishContainer = queryFirst([
|
||
"div.note-main-content-container",
|
||
".note-publish-wrap",
|
||
]);
|
||
if (publishContainer) {
|
||
try {
|
||
publishContainer.scrollIntoView({behavior: "smooth", block: "end"});
|
||
} catch (_) {
|
||
}
|
||
await sleep(Math.floor(Math.random() * 2001) + 1000);
|
||
}
|
||
|
||
// B11: 点击发布
|
||
console.log("[千帆助手] STEP 11: 点击发布按钮");
|
||
await sleep(Math.floor(Math.random() * 2001) + 1000);
|
||
const publishBtn = queryFirst([S.publishButtonExact, ".outarea.publish-c .submit .publishBtn"])
|
||
|| findButtonByText("发布") || findButtonByText("确认发布")
|
||
|| queryFirst([".publishBtn > .d-button-content", ".publishBtn"]);
|
||
if (!publishBtn) {
|
||
alertSelectorFailure("publish_button", ".publishBtn > .d-button-content", "publish button not found",
|
||
[".publishBtn > .d-button-content", ".publishBtn", 'button:发布']);
|
||
throw new Error("未找到发布按钮");
|
||
}
|
||
clickElement(publishBtn);
|
||
console.log("[千帆助手] 已点击发布按钮");
|
||
|
||
const result = await waitForPublishResult();
|
||
if (!result.success) {
|
||
throw new Error(result.error || "发布后未检测到成功状态");
|
||
}
|
||
|
||
// 停在"笔记发布成功"页的话,需要手动点继续按钮才会跳回选品页,
|
||
// 之前这里完全没处理,导致每次发完一篇就停在成功页,看起来像"卡住/暂停了"。
|
||
let continuedToItemSelect = isItemSelectPage();
|
||
if (result.onSuccessPage || isPublishSuccessPage()) {
|
||
continuedToItemSelect = await continueFromPublishSuccessPage();
|
||
}
|
||
|
||
console.log(`[千帆助手] 发布完成: ${note.title}`);
|
||
return {success: true, publishedAt: new Date().toISOString(), continuedToItemSelect};
|
||
}
|
||
|
||
// Selector failure alert(含页面同类型元素摘要,便于现场调选择器)
|
||
function alertSelectorFailure(step, selector, error, triedSelectors) {
|
||
const tried = Array.isArray(triedSelectors) && triedSelectors.length ? triedSelectors : [selector];
|
||
const ctx = {
|
||
url: window.location.href,
|
||
tried,
|
||
inputs: Array.from(document.querySelectorAll("input")).slice(0, 20).map((el) => ({
|
||
cls: el.className,
|
||
ph: el.placeholder,
|
||
type: el.type
|
||
})),
|
||
buttons: Array.from(document.querySelectorAll("button")).slice(0, 20).map((el) => ({
|
||
cls: el.className,
|
||
text: el.textContent.trim().slice(0, 20)
|
||
})),
|
||
editables: Array.from(document.querySelectorAll('[contenteditable="true"], .ql-editor, .ProseMirror')).slice(0, 10).map((el) => ({
|
||
tag: el.tagName,
|
||
cls: el.className
|
||
})),
|
||
links: Array.from(document.querySelectorAll("a")).slice(0, 20).map((el) => ({
|
||
cls: el.className,
|
||
text: el.textContent.trim().slice(0, 20)
|
||
})),
|
||
dTexts: Array.from(document.querySelectorAll("[class*='d-text']")).slice(0, 20).map((el) => ({
|
||
tag: el.tagName,
|
||
cls: el.className
|
||
})),
|
||
};
|
||
console.error(`[千帆助手] 选择器失效: ${step}`, {selector, error, ctx});
|
||
console.error(`[千帆助手] 可复制诊断信息(${step}):\n` + JSON.stringify(ctx, null, 2));
|
||
chrome.runtime.sendMessage({
|
||
type: "SELECTOR_FAILURE",
|
||
step,
|
||
selector,
|
||
error,
|
||
url: ctx.url,
|
||
timestamp: new Date().toISOString(),
|
||
}).catch(() => {
|
||
});
|
||
}
|
||
|
||
// ==================== 页面状态检测 ====================
|
||
|
||
function isItemSelectPage() {
|
||
return window.location.href.includes("publish-item-select") ||
|
||
window.location.href.includes("step=itemSelect");
|
||
}
|
||
|
||
function isPublishSuccessPage() {
|
||
return window.location.href.includes("publish-success");
|
||
}
|
||
|
||
function isPublishPage() {
|
||
const url = window.location.href;
|
||
// Must be on千帆 AND be on a publish page, NOT the item-select page, NOT the success page
|
||
if (!url.includes("ark.xiaohongshu.com")) return false;
|
||
if (url.includes("publish-item-select") || url.includes("step=itemSelect")) return false;
|
||
if (url.includes("publish-success")) return false;
|
||
return url.includes("/app-note/publish") ||
|
||
url.includes("step=notePublish") ||
|
||
url.includes("mainPublishType=") ||
|
||
url.includes("step=contentEdit");
|
||
}
|
||
|
||
// ==================== 消息监听 ====================
|
||
|
||
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
||
if (msg.type === "EXECUTE_PUBLISH") {
|
||
executePublish(msg.task)
|
||
.then((result) => sendResponse({success: true, result}))
|
||
.catch((e) => sendResponse({success: false, error: e.message}));
|
||
return true;
|
||
}
|
||
|
||
if (msg.type === "PING") {
|
||
sendResponse({
|
||
ok: true,
|
||
url: window.location.href,
|
||
isItemSelectPage: isItemSelectPage(),
|
||
isPublishPage: isPublishPage(),
|
||
hasEditor: !!document.querySelector('[contenteditable="true"], .ql-editor'),
|
||
hasUploadInput: !!document.querySelector('input[type="file"]'),
|
||
hasSearchInput: !!document.querySelector('input[placeholder*="商品"]'),
|
||
});
|
||
return false;
|
||
}
|
||
|
||
if (msg.type === "GET_PAGE_INFO") {
|
||
const info = {
|
||
url: window.location.href,
|
||
isItemSelectPage: isItemSelectPage(),
|
||
isPublishPage: isPublishPage(),
|
||
hasEditor: !!document.querySelector('[contenteditable="true"], .ql-editor'),
|
||
hasUploadInput: !!document.querySelector('input[type="file"]'),
|
||
hasPublishButton: !!findButtonByText("发布"),
|
||
hasSearchInput: !!document.querySelector('input[placeholder*="商品"]'),
|
||
};
|
||
sendResponse(info);
|
||
return false;
|
||
}
|
||
});
|
||
|
||
console.log("[千帆助手] 内容脚本 v2 已加载,URL:", window.location.href);
|