Compare commits

...

4 Commits

18 changed files with 1597 additions and 217 deletions
+56 -10
View File
@@ -18,10 +18,6 @@ function normalizeString(value) {
return normalized || undefined
}
function normalizeText(value) {
return String(value || '').replace(/\s+/g, '')
}
function parseAliyunPublishedUrl(rawUrl) {
if (!rawUrl) return null
@@ -100,8 +96,41 @@ function inspectAliyunTaskState(rawUrl, publicUrl, draftSaved = false) {
function fillAliyunContent(title, markdown, html, plainText) {
const markdownContent = markdown || ''
const normalizeText = value => String(value || '').replace(/\s+/g, '')
const normalizeContent = value => String(value || '').replace(/\r\n?/g, '\n')
const normalizeComparableText = value => String(value || '')
.normalize('NFKC')
.replace(/[^\p{L}\p{N}]+/gu, '')
const sleep = milliseconds => new Promise(resolve => setTimeout(resolve, milliseconds))
function htmlToComparableText(value) {
const entities = {
'&': '&',
'>': '>',
'&lt;': '<',
'&quot;': '"',
'&#39;': "'",
'&nbsp;': ' ',
}
return normalizeComparableText(String(value || '')
.replace(/<img\b[^>]*>/gi, '')
.replace(/<[^>]+>/g, ' ')
.replace(/&(?:amp|gt|lt|quot|#39|nbsp);/g, entity => entities[entity] || entity))
}
const expectedRichText = html
? htmlToComparableText(html)
: normalizeComparableText(plainText || markdownContent)
const expectedImageCount = Array.from(String(html || '').matchAll(/<img\b[^>]*>/gi)).length
const minimumTextLength = Math.floor(expectedRichText.length * 0.8)
const sampleLength = Math.min(24, Math.max(6, Math.floor(expectedRichText.length / 4)))
const sampleStarts = expectedRichText.length > sampleLength
? [0, Math.floor((expectedRichText.length - sampleLength) / 2), expectedRichText.length - sampleLength]
: [0]
const expectedSamples = [...new Set(sampleStarts.map(start => (
expectedRichText.slice(start, start + sampleLength)
)).filter(Boolean))]
function findFirst(selectors) {
for (const selector of selectors) {
const element = Array.from(document.querySelectorAll(selector))
@@ -173,15 +202,23 @@ function fillAliyunContent(title, markdown, html, plainText) {
}
function hasExpectedMarkdown(value) {
const minimumLength = Math.min(Math.max(12, Math.floor(normalizeText(markdownContent).length * 0.1)), 120)
return normalizeText(value).length >= minimumLength
return normalizeContent(value) === normalizeContent(markdownContent)
}
function hasExpectedTitle(input) {
const expectedTitle = normalizeText(title)
return expectedTitle.length === 0 || normalizeText(input?.value) === expectedTitle
}
function hasExpectedRichText(editor) {
const textLength = normalizeText(editor?.innerText || editor?.textContent).length
const editorText = normalizeComparableText(editor?.innerText || editor?.textContent)
const imageCount = editor?.querySelectorAll?.('img').length || 0
const minimumLength = Math.min(Math.max(12, Math.floor(normalizeText(plainText).length * 0.1)), 120)
return textLength + imageCount * 12 >= minimumLength
const receivedText = expectedRichText.length === 0 || (
editorText.length >= minimumTextLength
&& expectedSamples.every(sample => editorText.includes(sample))
)
const receivedImages = expectedImageCount === 0 || imageCount >= expectedImageCount
return receivedText && receivedImages
}
async function switchToMarkdownEditor() {
@@ -206,7 +243,13 @@ function fillAliyunContent(title, markdown, html, plainText) {
'.article-title input',
])
if (!titleInput) return { success: false, error: '未找到阿里云开发者社区标题输入框' }
if (title) setInputValue(titleInput, title)
if (title) {
setInputValue(titleInput, title)
await sleep(200)
if (!hasExpectedTitle(titleInput)) {
return { success: false, error: '阿里云开发者社区未确认接收标题' }
}
}
if (!markdownContent) return { success: true, method: 'title-only' }
await switchToMarkdownEditor()
@@ -220,6 +263,9 @@ function fillAliyunContent(title, markdown, html, plainText) {
}
const textarea = await waitForElement([
'.editor textarea.textarea',
'textarea#article-editor',
'textarea.textarea',
'.markdown-editor textarea',
'.editor-container textarea',
'textarea[placeholder*="正文"]',
+43 -4
View File
@@ -92,6 +92,35 @@ function inspectBaijiahaoTaskState(rawUrl, publicUrl) {
function fillBaijiahaoContent(title, html, plainText) {
const sleep = milliseconds => new Promise(resolve => setTimeout(resolve, milliseconds))
const normalizeText = value => String(value || '').replace(/\s+/g, '')
const normalizeComparableText = value => String(value || '')
.normalize('NFKC')
.replace(/[^\p{L}\p{N}]+/gu, '')
function htmlToComparableText(value) {
const entities = {
'&amp;': '&',
'&gt;': '>',
'&lt;': '<',
'&quot;': '"',
'&#39;': "'",
'&nbsp;': ' ',
}
return normalizeComparableText(String(value || '')
.replace(/<img\b[^>]*>/gi, '')
.replace(/<[^>]+>/g, ' ')
.replace(/&(?:amp|gt|lt|quot|#39|nbsp);/g, entity => entities[entity] || entity))
}
const expectedText = htmlToComparableText(html)
const expectedImageCount = Array.from(String(html || '').matchAll(/<img\b[^>]*>/gi)).length
const minimumTextLength = Math.floor(expectedText.length * 0.8)
const sampleLength = Math.min(24, Math.max(6, Math.floor(expectedText.length / 4)))
const sampleStarts = expectedText.length > sampleLength
? [0, Math.floor((expectedText.length - sampleLength) / 2), expectedText.length - sampleLength]
: [0]
const expectedSamples = [...new Set(sampleStarts.map(start => (
expectedText.slice(start, start + sampleLength)
)).filter(Boolean))]
function waitForCondition(getValue, timeout = 15000) {
return new Promise(resolve => {
@@ -168,9 +197,13 @@ function fillBaijiahaoContent(title, html, plainText) {
}
function hasExpectedContent(text, imageCount) {
const expectedTextLength = normalizeText(plainText).length
const minimumLength = Math.min(Math.max(12, Math.floor(expectedTextLength * 0.1)), 120)
return normalizeText(text).length + imageCount * 12 >= minimumLength
const editorText = normalizeComparableText(text)
const receivedText = expectedText.length === 0 || (
editorText.length >= minimumTextLength
&& expectedSamples.every(sample => editorText.includes(sample))
)
const receivedImages = expectedImageCount === 0 || imageCount >= expectedImageCount
return receivedText && receivedImages
}
async function fill() {
@@ -181,7 +214,13 @@ function fillBaijiahaoContent(title, html, plainText) {
|| document.querySelector('[contenteditable="true"][placeholder*="标题"]')
))
if (!titleEditor) return { success: false, error: '未找到百家号标题输入框' }
if (title) setEditableText(titleEditor, title)
if (title) {
setEditableText(titleEditor, title)
await sleep(200)
if (normalizeText(titleEditor.textContent) !== normalizeText(title)) {
return { success: false, error: '百家号未确认接收标题' }
}
}
if (!html) return { success: true, method: 'title-only' }
+4 -5
View File
@@ -70,6 +70,7 @@ function fillCsdnContent(title, markdown, body) {
const contentToFill = markdown || body || ''
const sleep = milliseconds => new Promise(resolve => setTimeout(resolve, milliseconds))
const normalizeText = value => String(value || '').replace(/\s+/g, '')
const normalizeContent = value => String(value || '').replace(/\r\n?/g, '\n')
function findFirst(selectors, excludedElement) {
for (const selector of selectors) {
@@ -142,10 +143,8 @@ function fillCsdnContent(title, markdown, body) {
}
function hasExpectedContent(editor) {
const expectedLength = normalizeText(contentToFill).length
const minimumLength = Math.min(Math.max(12, Math.floor(expectedLength * 0.1)), 120)
const actualLength = normalizeText(editor?.value || editor?.innerText || editor?.textContent).length
return actualLength >= minimumLength
const actualContent = editor?.value ?? editor?.innerText ?? editor?.textContent
return normalizeContent(actualContent) === normalizeContent(contentToFill)
}
function hasExpectedTitle(input) {
@@ -199,7 +198,7 @@ function fillCsdnContent(title, markdown, body) {
editor.CodeMirror.setValue(contentToFill)
editor.CodeMirror.focus?.()
await sleep(500)
if (hasExpectedContent(editor.CodeMirror.getWrapperElement?.() || editor)) {
if (normalizeContent(editor.CodeMirror.getValue?.()) === normalizeContent(contentToFill)) {
return { success: true, method: 'codemirror' }
}
} else if (editor instanceof HTMLTextAreaElement) {
+42 -10
View File
@@ -11,10 +11,6 @@ const CTO51Platform = {
type: '51cto',
}
function normalizeText(value) {
return String(value || '').replace(/\s+/g, '')
}
function parseCto51PublishedUrl(rawUrl) {
if (!rawUrl) return null
@@ -79,8 +75,41 @@ function inspectCto51TaskState(rawUrl, publicUrl, draftSaved = false) {
function fillCto51Content(title, markdown, html, plainText) {
const markdownContent = markdown || ''
const normalizeText = value => String(value || '').replace(/\s+/g, '')
const normalizeContent = value => String(value || '').replace(/\r\n?/g, '\n')
const normalizeComparableText = value => String(value || '')
.normalize('NFKC')
.replace(/[^\p{L}\p{N}]+/gu, '')
const sleep = milliseconds => new Promise(resolve => setTimeout(resolve, milliseconds))
function htmlToComparableText(value) {
const entities = {
'&amp;': '&',
'&gt;': '>',
'&lt;': '<',
'&quot;': '"',
'&#39;': "'",
'&nbsp;': ' ',
}
return normalizeComparableText(String(value || '')
.replace(/<img\b[^>]*>/gi, '')
.replace(/<[^>]+>/g, ' ')
.replace(/&(?:amp|gt|lt|quot|#39|nbsp);/g, entity => entities[entity] || entity))
}
const expectedRichText = html
? htmlToComparableText(html)
: normalizeComparableText(plainText || markdownContent)
const expectedImageCount = Array.from(String(html || '').matchAll(/<img\b[^>]*>/gi)).length
const minimumTextLength = Math.floor(expectedRichText.length * 0.8)
const sampleLength = Math.min(24, Math.max(6, Math.floor(expectedRichText.length / 4)))
const sampleStarts = expectedRichText.length > sampleLength
? [0, Math.floor((expectedRichText.length - sampleLength) / 2), expectedRichText.length - sampleLength]
: [0]
const expectedSamples = [...new Set(sampleStarts.map(start => (
expectedRichText.slice(start, start + sampleLength)
)).filter(Boolean))]
function findFirst(selectors) {
for (const selector of selectors) {
const element = Array.from(document.querySelectorAll(selector))
@@ -148,15 +177,18 @@ function fillCto51Content(title, markdown, html, plainText) {
}
function hasExpectedMarkdown(value) {
const minimumLength = Math.min(Math.max(12, Math.floor(normalizeText(markdownContent).length * 0.1)), 120)
return normalizeText(value).length >= minimumLength
return normalizeContent(value) === normalizeContent(markdownContent)
}
function hasExpectedRichText(editor) {
const textLength = normalizeText(editor?.innerText || editor?.textContent).length
const editorText = normalizeComparableText(editor?.innerText || editor?.textContent)
const imageCount = editor?.querySelectorAll?.('img').length || 0
const minimumLength = Math.min(Math.max(12, Math.floor(normalizeText(plainText).length * 0.1)), 120)
return textLength + imageCount * 12 >= minimumLength
const receivedText = expectedRichText.length === 0 || (
editorText.length >= minimumTextLength
&& expectedSamples.every(sample => editorText.includes(sample))
)
const receivedImages = expectedImageCount === 0 || imageCount >= expectedImageCount
return receivedText && receivedImages
}
async function fill() {
@@ -170,7 +202,7 @@ function fillCto51Content(title, markdown, html, plainText) {
if (title) {
setInputValue(titleInput, title)
await sleep(100)
if (!normalizeText(titleInput.value).includes(normalizeText(title).slice(0, 12))) {
if (normalizeText(titleInput.value) !== normalizeText(title)) {
return { success: false, error: '51CTO 未确认接收标题' }
}
}
+60 -12
View File
@@ -138,21 +138,59 @@ function fillInfoQContent(title, markdown, html, plainText) {
const contentToFill = markdown || ''
const sleep = milliseconds => new Promise(resolve => setTimeout(resolve, milliseconds))
const normalizeText = value => String(value || '').replace(/\s+/g, '')
const normalizeComparableText = value => String(value || '')
.normalize('NFKC')
.replace(/[^\p{L}\p{N}]+/gu, '')
function findFirst(selectors) {
return selectors.map(selector => document.querySelector(selector)).find(Boolean)
function htmlToComparableText(value) {
const entities = {
'&amp;': '&',
'&gt;': '>',
'&lt;': '<',
'&quot;': '"',
'&#39;': "'",
'&nbsp;': ' ',
}
return normalizeComparableText(String(value || '')
.replace(/<img\b[^>]*>/gi, '')
.replace(/<[^>]+>/g, ' ')
.replace(/&(?:amp|gt|lt|quot|#39|nbsp);/g, entity => entities[entity] || entity))
}
function waitForElement(selectors, timeout = 15000) {
const expectedText = htmlToComparableText(html)
const expectedImageCount = Array.from(String(html || '').matchAll(/<img\b[^>]*>/gi)).length
const minimumTextLength = Math.floor(expectedText.length * 0.8)
const sampleLength = Math.min(24, Math.max(6, Math.floor(expectedText.length / 4)))
const sampleStarts = expectedText.length > sampleLength
? [0, Math.floor((expectedText.length - sampleLength) / 2), expectedText.length - sampleLength]
: [0]
const expectedSamples = [...new Set(sampleStarts.map(start => (
expectedText.slice(start, start + sampleLength)
)).filter(Boolean))]
function findFirst(selectors, excludedElement) {
for (const selector of selectors) {
const element = Array.from(document.querySelectorAll(selector))
.find(candidate => (
candidate !== excludedElement
&& !candidate.disabled
&& candidate.offsetParent !== null
))
if (element) return element
}
return null
}
function waitForElement(selectors, excludedElement, timeout = 15000) {
return new Promise(resolve => {
const existing = findFirst(selectors)
const existing = findFirst(selectors, excludedElement)
if (existing) {
resolve(existing)
return
}
const observer = new MutationObserver(() => {
const element = findFirst(selectors)
const element = findFirst(selectors, excludedElement)
if (!element) return
observer.disconnect()
resolve(element)
@@ -160,7 +198,7 @@ function fillInfoQContent(title, markdown, html, plainText) {
observer.observe(document.documentElement, { childList: true, subtree: true })
window.setTimeout(() => {
observer.disconnect()
resolve(findFirst(selectors))
resolve(findFirst(selectors, excludedElement))
}, timeout)
})
}
@@ -199,11 +237,14 @@ function fillInfoQContent(title, markdown, html, plainText) {
}
function hasExpectedContent(editor) {
const expectedTextLength = normalizeText(plainText).length
const minimumLength = Math.min(Math.max(12, Math.floor(expectedTextLength * 0.1)), 120)
const textLength = normalizeText(editor?.innerText || editor?.textContent).length
const editorText = normalizeComparableText(editor?.innerText || editor?.textContent)
const imageCount = editor?.querySelectorAll?.('img').length || 0
return textLength + imageCount * 12 >= minimumLength
const receivedText = expectedText.length === 0 || (
editorText.length >= minimumTextLength
&& expectedSamples.every(sample => editorText.includes(sample))
)
const receivedImages = expectedImageCount === 0 || imageCount >= expectedImageCount
return receivedText && receivedImages
}
function getVueCandidates(editor) {
@@ -242,7 +283,14 @@ function fillInfoQContent(title, markdown, html, plainText) {
'[contenteditable="true"][placeholder*="标题"]',
])
if (!titleInput) return { success: false, error: '未找到 InfoQ 标题输入框' }
if (title) setInputValue(titleInput, title)
if (title) {
setInputValue(titleInput, title)
await sleep(200)
const receivedTitle = titleInput.isContentEditable ? titleInput.textContent : titleInput.value
if (normalizeText(receivedTitle) !== normalizeText(title)) {
return { success: false, error: 'InfoQ 未确认接收标题' }
}
}
const editor = await waitForElement([
'.gk-editor [contenteditable="true"]',
@@ -250,7 +298,7 @@ function fillInfoQContent(title, markdown, html, plainText) {
'.ProseMirror[contenteditable="true"]',
'.ProseMirror',
'[contenteditable="true"][role="textbox"]',
])
], titleInput)
if (!editor) return { success: false, error: '未找到 InfoQ 正文编辑器' }
if (!contentToFill) return { success: true, method: 'title-only' }
+82 -30
View File
@@ -1,5 +1,3 @@
import { injectUtils } from './common.js'
// 简书平台配置
const JianshuPlatform = {
id: 'jianshu',
@@ -99,39 +97,94 @@ async function inspectJianshuTask(tab, helpers) {
function fillJianshuContent(title, markdown, body) {
const contentToFill = markdown || body || ''
const normalizeContent = value => String(value || '').replace(/\r\n?/g, '\n')
const sleep = milliseconds => new Promise(resolve => setTimeout(resolve, milliseconds))
function findFirst(selectors, excludedElement) {
for (const selector of selectors) {
const element = Array.from(document.querySelectorAll(selector))
.find(candidate => (
candidate !== excludedElement
&& !candidate.disabled
&& candidate.offsetParent !== null
))
if (element) return element
}
return null
}
function waitForElement(selectors, excludedElement, timeout = 15000) {
return new Promise(resolve => {
const existing = findFirst(selectors, excludedElement)
if (existing) {
resolve(existing)
return
}
const observer = new MutationObserver(() => {
const element = findFirst(selectors, excludedElement)
if (!element) return
observer.disconnect()
resolve(element)
})
observer.observe(document.documentElement, { childList: true, subtree: true })
window.setTimeout(() => {
observer.disconnect()
resolve(findFirst(selectors, excludedElement))
}, timeout)
})
}
function setInputValue(input, value) {
const prototype = input instanceof HTMLTextAreaElement
? HTMLTextAreaElement.prototype
: HTMLInputElement.prototype
const setter = Object.getOwnPropertyDescriptor(prototype, 'value')?.set
|| Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value')?.set
input.focus()
if (setter) setter.call(input, value)
else input.value = value
input.dispatchEvent(new InputEvent('input', {
bubbles: true,
data: value,
inputType: 'insertText',
}))
input.dispatchEvent(new Event('change', { bubbles: true }))
input.dispatchEvent(new Event('blur', { bubbles: true }))
}
async function fill() {
const titleInput = await window.waitFor('input._24i7u, input[class*="title"]')
if (titleInput && title) {
titleInput.focus()
const inputSetter = Object.getOwnPropertyDescriptor(
window.HTMLInputElement.prototype,
'value',
)?.set
inputSetter?.call(titleInput, title)
titleInput.dispatchEvent(
new InputEvent('input', { bubbles: true, data: title, inputType: 'insertText' }),
)
titleInput.dispatchEvent(new Event('change', { bubbles: true }))
titleInput.dispatchEvent(new Event('blur', { bubbles: true }))
const titleInput = title
? await waitForElement([
'input[placeholder*="标题"]',
'textarea[placeholder*="标题"]',
'input._24i7u',
'input[class*="title"]',
])
: null
if (title) {
if (!titleInput) return { success: false, error: '未找到简书标题输入框' }
setInputValue(titleInput, title)
await sleep(200)
if (normalizeContent(titleInput.value) !== normalizeContent(title)) {
return { success: false, error: '简书未确认接收标题' }
}
}
await new Promise(resolve => setTimeout(resolve, 500))
if (!contentToFill) return { success: true, method: 'title-only' }
const editor = document.querySelector('#arthur-editor')
|| document.querySelector('textarea._3swFR')
if (!editor) return { success: false, error: 'Editor not found' }
const editor = await waitForElement([
'textarea#arthur-editor',
'textarea._3swFR',
], titleInput, 5000)
if (!editor) return { success: false, error: '未找到简书 Markdown 编辑器' }
setInputValue(editor, contentToFill)
await sleep(300)
if (normalizeContent(editor.value) !== normalizeContent(contentToFill)) {
return { success: false, error: '简书未确认接收正文' }
}
editor.focus()
const textareaSetter = Object.getOwnPropertyDescriptor(
window.HTMLTextAreaElement.prototype,
'value',
)?.set
textareaSetter?.call(editor, contentToFill)
editor.dispatchEvent(
new InputEvent('input', { bubbles: true, data: contentToFill, inputType: 'insertText' }),
)
editor.dispatchEvent(new Event('change', { bubbles: true }))
return { success: true, method: 'textarea' }
}
@@ -181,7 +234,6 @@ async function syncJianshuContent(tab, content, helpers) {
const targetUrl = `https://www.jianshu.com/writer#/notebooks/${notebookId}/notes/${note.id}`
const updatedTab = await chrome.tabs.update(tab.id, { url: targetUrl })
await waitForTab(updatedTab.id)
await injectUtils(chrome, updatedTab.id)
const result = await chrome.scripting.executeScript({
target: { tabId: updatedTab.id },
+99 -39
View File
@@ -9,8 +9,6 @@ const JuejinPlatform = {
type: 'juejin',
}
import { injectUtils } from './common.js'
function inspectJuejinTaskState(rawUrl) {
let url
try {
@@ -60,43 +58,110 @@ function inspectJuejinTaskState(rawUrl) {
}
}
// 掘金内容填充函数(在页面主世界中执行)
// 注意:需要先调用 injectUtils 注入 window.waitFor
function fillJuejinContent(title, markdown, body) {
const contentToFill = markdown || body || ''
const normalizeText = value => String(value || '').replace(/\s+/g, '')
const normalizeContent = value => String(value || '').replace(/\r\n?/g, '\n')
const sleep = milliseconds => new Promise(resolve => setTimeout(resolve, milliseconds))
function findFirst(selectors, excludedElement) {
for (const selector of selectors) {
const element = Array.from(document.querySelectorAll(selector))
.find(candidate => (
candidate !== excludedElement
&& !candidate.disabled
&& candidate.offsetParent !== null
))
if (element) return element
}
return null
}
function waitForElement(selectors, excludedElement, timeout = 15000) {
return new Promise(resolve => {
const existing = findFirst(selectors, excludedElement)
if (existing) {
resolve(existing)
return
}
const observer = new MutationObserver(() => {
const element = findFirst(selectors, excludedElement)
if (!element) return
observer.disconnect()
resolve(element)
})
observer.observe(document.documentElement, { childList: true, subtree: true })
window.setTimeout(() => {
observer.disconnect()
resolve(findFirst(selectors, excludedElement))
}, timeout)
})
}
function setInputValue(input, value) {
const prototype = input instanceof HTMLTextAreaElement
? HTMLTextAreaElement.prototype
: HTMLInputElement.prototype
const setter = Object.getOwnPropertyDescriptor(prototype, 'value')?.set
|| Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value')?.set
input.focus()
if (setter) setter.call(input, value)
else input.value = value
input.dispatchEvent(new InputEvent('input', {
bubbles: true,
data: value,
inputType: 'insertText',
}))
input.dispatchEvent(new Event('change', { bubbles: true }))
input.dispatchEvent(new Event('blur', { bubbles: true }))
}
async function fill() {
// 填充标题(使用注入的 window.waitFor
const titleInput = await window.waitFor('input[placeholder*="标题"]')
if (titleInput && title) {
titleInput.focus()
titleInput.value = title
titleInput.dispatchEvent(new Event('input', { bubbles: true }))
}
// 等待编辑器加载
await new Promise(resolve => setTimeout(resolve, 1000))
// 掘金使用 ByteMD 编辑器(基于 CodeMirror
const cmElement = document.querySelector('.CodeMirror')
if (cmElement && cmElement.CodeMirror) {
cmElement.CodeMirror.setValue(contentToFill)
console.log('[COSE] 掘金 CodeMirror 填充成功')
return { success: true, method: 'CodeMirror' }
} else {
// 降级到 textarea
const textarea = document.querySelector('.bytemd-body textarea')
if (textarea) {
textarea.focus()
textarea.value = contentToFill
textarea.dispatchEvent(new Event('input', { bubbles: true }))
console.log('[COSE] 掘金 textarea 填充成功')
return { success: true, method: 'textarea' }
} else {
console.log('[COSE] 掘金 未找到编辑器')
return { success: false, error: 'Editor not found' }
const titleInput = title
? await waitForElement([
'input[placeholder*="标题"]',
'textarea[placeholder*="标题"]',
'.title-input input',
'input.title-input',
])
: null
if (title) {
if (!titleInput) return { success: false, error: '未找到掘金标题输入框' }
setInputValue(titleInput, title)
await sleep(200)
if (normalizeText(titleInput.value) !== normalizeText(title)) {
return { success: false, error: '掘金未确认接收标题' }
}
}
if (!contentToFill) return { success: true, method: 'title-only' }
const cmElement = await waitForElement(['.CodeMirror'], titleInput, 3000)
if (cmElement && cmElement.CodeMirror) {
cmElement.CodeMirror.setValue(contentToFill)
cmElement.CodeMirror.focus?.()
await sleep(300)
if (normalizeContent(cmElement.CodeMirror.getValue?.()) === normalizeContent(contentToFill)) {
return { success: true, method: 'codemirror' }
}
return { success: false, error: '掘金未确认接收正文' }
}
const textarea = await waitForElement([
'.bytemd-body textarea',
'.bytemd-editor textarea',
'.bytemd-split .bytemd-body textarea',
], titleInput, 4000)
if (!textarea) return { success: false, error: '未找到掘金 Markdown 编辑器' }
setInputValue(textarea, contentToFill)
await sleep(300)
if (normalizeContent(textarea.value) !== normalizeContent(contentToFill)) {
return { success: false, error: '掘金未确认接收正文' }
}
return { success: true, method: 'textarea' }
}
return fill()
@@ -112,13 +177,8 @@ function fillJuejinContent(title, markdown, body) {
async function syncJuejinContent(tab, content, helpers) {
const { chrome } = helpers
// 等待页面加载
await new Promise(resolve => setTimeout(resolve, 2000))
await helpers.waitForTab(tab.id)
// 先注入公共工具函数(waitFor, setInputValue
await injectUtils(chrome, tab.id)
// 在页面中执行填充脚本
const result = await chrome.scripting.executeScript({
target: { tabId: tab.id },
func: fillJuejinContent,
+42 -10
View File
@@ -10,10 +10,6 @@ const OSChinaPlatform = {
type: 'oschina',
}
function normalizeText(value) {
return String(value || '').replace(/\s+/g, '')
}
function isOSChinaEditorUrl(url) {
return url.protocol === 'https:'
&& url.hostname === 'my.oschina.net'
@@ -57,8 +53,41 @@ function inspectOSChinaTaskState(rawUrl, draftSaved = false) {
function fillOSChinaContent(title, markdown, html, plainText) {
const markdownContent = markdown || ''
const normalizeText = value => String(value || '').replace(/\s+/g, '')
const normalizeContent = value => String(value || '').replace(/\r\n?/g, '\n')
const normalizeComparableText = value => String(value || '')
.normalize('NFKC')
.replace(/[^\p{L}\p{N}]+/gu, '')
const sleep = milliseconds => new Promise(resolve => setTimeout(resolve, milliseconds))
function htmlToComparableText(value) {
const entities = {
'&amp;': '&',
'&gt;': '>',
'&lt;': '<',
'&quot;': '"',
'&#39;': "'",
'&nbsp;': ' ',
}
return normalizeComparableText(String(value || '')
.replace(/<img\b[^>]*>/gi, '')
.replace(/<[^>]+>/g, ' ')
.replace(/&(?:amp|gt|lt|quot|#39|nbsp);/g, entity => entities[entity] || entity))
}
const expectedRichText = html
? htmlToComparableText(html)
: normalizeComparableText(plainText || markdownContent)
const expectedImageCount = Array.from(String(html || '').matchAll(/<img\b[^>]*>/gi)).length
const minimumTextLength = Math.floor(expectedRichText.length * 0.8)
const sampleLength = Math.min(24, Math.max(6, Math.floor(expectedRichText.length / 4)))
const sampleStarts = expectedRichText.length > sampleLength
? [0, Math.floor((expectedRichText.length - sampleLength) / 2), expectedRichText.length - sampleLength]
: [0]
const expectedSamples = [...new Set(sampleStarts.map(start => (
expectedRichText.slice(start, start + sampleLength)
)).filter(Boolean))]
function findFirst(selectors) {
for (const selector of selectors) {
const element = Array.from(document.querySelectorAll(selector))
@@ -126,15 +155,18 @@ function fillOSChinaContent(title, markdown, html, plainText) {
}
function hasExpectedMarkdown(value) {
const minimumLength = Math.min(Math.max(12, Math.floor(normalizeText(markdownContent).length * 0.1)), 120)
return normalizeText(value).length >= minimumLength
return normalizeContent(value) === normalizeContent(markdownContent)
}
function hasExpectedRichText(editor) {
const textLength = normalizeText(editor?.innerText || editor?.textContent).length
const editorText = normalizeComparableText(editor?.innerText || editor?.textContent)
const imageCount = editor?.querySelectorAll?.('img').length || 0
const minimumLength = Math.min(Math.max(12, Math.floor(normalizeText(plainText).length * 0.1)), 120)
return textLength + imageCount * 12 >= minimumLength
const receivedText = expectedRichText.length === 0 || (
editorText.length >= minimumTextLength
&& expectedSamples.every(sample => editorText.includes(sample))
)
const receivedImages = expectedImageCount === 0 || imageCount >= expectedImageCount
return receivedText && receivedImages
}
async function switchToMarkdownEditor() {
@@ -169,7 +201,7 @@ function fillOSChinaContent(title, markdown, html, plainText) {
if (title) {
setInputValue(titleInput, title)
await sleep(100)
if (!normalizeText(titleInput.value).includes(normalizeText(title).slice(0, 12))) {
if (normalizeText(titleInput.value) !== normalizeText(title)) {
return { success: false, error: '开源中国未确认接收标题' }
}
}
+41 -11
View File
@@ -16,10 +16,6 @@ function normalizeString(value) {
return normalized || undefined
}
function normalizeText(value) {
return String(value || '').replace(/\s+/g, '')
}
function getQiehaoDraftId(url) {
return normalizeString(
url.searchParams.get('draftId')
@@ -98,8 +94,40 @@ function inspectQiehaoTaskState(rawUrl, draftSaved = false) {
function fillQiehaoContent(title, html, plainText) {
const contentHtml = html || ''
const contentText = plainText || contentHtml.replace(/<[^>]+>/g, ' ')
const normalizeText = value => String(value || '').replace(/\s+/g, '')
const normalizeComparableText = value => String(value || '')
.normalize('NFKC')
.replace(/[^\p{L}\p{N}]+/gu, '')
const sleep = milliseconds => new Promise(resolve => setTimeout(resolve, milliseconds))
function htmlToComparableText(value) {
const entities = {
'&amp;': '&',
'&gt;': '>',
'&lt;': '<',
'&quot;': '"',
'&#39;': "'",
'&nbsp;': ' ',
}
return normalizeComparableText(String(value || '')
.replace(/<img\b[^>]*>/gi, '')
.replace(/<[^>]+>/g, ' ')
.replace(/&(?:amp|gt|lt|quot|#39|nbsp);/g, entity => entities[entity] || entity))
}
const expectedRichText = contentHtml
? htmlToComparableText(contentHtml)
: normalizeComparableText(contentText)
const expectedImageCount = Array.from(contentHtml.matchAll(/<img\b[^>]*>/gi)).length
const minimumTextLength = Math.floor(expectedRichText.length * 0.8)
const sampleLength = Math.min(24, Math.max(6, Math.floor(expectedRichText.length / 4)))
const sampleStarts = expectedRichText.length > sampleLength
? [0, Math.floor((expectedRichText.length - sampleLength) / 2), expectedRichText.length - sampleLength]
: [0]
const expectedSamples = [...new Set(sampleStarts.map(start => (
expectedRichText.slice(start, start + sampleLength)
)).filter(Boolean))]
function findFirst(selectors) {
return selectors
.map(selector => document.querySelector(selector))
@@ -151,17 +179,19 @@ function fillQiehaoContent(title, html, plainText) {
}
function hasExpectedTitle(editor) {
return normalizeText(editor?.textContent).length >= Math.min(1, normalizeText(title).length)
const expectedTitle = normalizeText(title)
return expectedTitle.length === 0 || normalizeText(editor?.textContent) === expectedTitle
}
function hasExpectedContent(editor) {
const expectedTextLength = normalizeText(contentText).length
const actualTextLength = normalizeText(editor?.innerText || editor?.textContent).length
const expectedImageCount = (contentHtml.match(/<img\b/gi) || []).length
const editorText = normalizeComparableText(editor?.innerText || editor?.textContent)
const actualImageCount = editor?.querySelectorAll?.('img').length || 0
const minimumTextLength = Math.min(Math.max(12, Math.floor(expectedTextLength * 0.1)), 120)
return (expectedTextLength === 0 || actualTextLength >= minimumTextLength)
&& actualImageCount >= expectedImageCount
const receivedText = expectedRichText.length === 0 || (
editorText.length >= minimumTextLength
&& expectedSamples.every(sample => editorText.includes(sample))
)
const receivedImages = expectedImageCount === 0 || actualImageCount >= expectedImageCount
return receivedText && receivedImages
}
async function fill() {
@@ -83,6 +83,7 @@ function inspectSegmentFaultTaskState(rawUrl, publicUrl, draftSaved = false) {
function fillSegmentFaultContent(title, markdown, body) {
const contentToFill = markdown || body || ''
const normalizeText = value => String(value || '').replace(/\s+/g, '')
const normalizeContent = value => String(value || '').replace(/\r\n?/g, '\n')
const sleep = milliseconds => new Promise(resolve => setTimeout(resolve, milliseconds))
function findFirst(selectors, excludedElement) {
@@ -151,11 +152,13 @@ function fillSegmentFaultContent(title, markdown, body) {
editor.dispatchEvent(new Event('blur', { bubbles: true }))
}
function hasExpectedContent(editor) {
const expectedLength = normalizeText(contentToFill).length
const minimumLength = Math.min(Math.max(12, Math.floor(expectedLength * 0.1)), 120)
const actualLength = normalizeText(editor?.value || editor?.innerText || editor?.textContent).length
return actualLength >= minimumLength
function hasExpectedTitle(input) {
const expectedTitle = normalizeText(title)
return expectedTitle.length === 0 || normalizeText(input?.value) === expectedTitle
}
function hasExpectedContent(value) {
return normalizeContent(value) === normalizeContent(contentToFill)
}
async function fill() {
@@ -166,7 +169,11 @@ function fillSegmentFaultContent(title, markdown, body) {
'.title-input input',
])
if (title && !titleInput) return { success: false, error: '未找到思否标题输入框' }
if (titleInput && title) setInputValue(titleInput, title)
if (titleInput && title) {
setInputValue(titleInput, title)
await sleep(200)
if (!hasExpectedTitle(titleInput)) return { success: false, error: '思否未确认接收标题' }
}
if (!contentToFill) return { success: true, method: 'title-only' }
@@ -185,17 +192,17 @@ function fillSegmentFaultContent(title, markdown, body) {
editor.CodeMirror.setValue(contentToFill)
editor.CodeMirror.focus?.()
await sleep(500)
if (hasExpectedContent(editor.CodeMirror.getWrapperElement?.() || editor)) {
if (hasExpectedContent(editor.CodeMirror.getValue?.())) {
return { success: true, method: 'codemirror' }
}
} else if (editor instanceof HTMLTextAreaElement) {
setInputValue(editor, contentToFill)
await sleep(300)
if (hasExpectedContent(editor)) return { success: true, method: 'textarea' }
if (hasExpectedContent(editor.value)) return { success: true, method: 'textarea' }
} else if (editor.isContentEditable) {
setContentEditableValue(editor, contentToFill)
await sleep(300)
if (hasExpectedContent(editor)) return { success: true, method: 'contenteditable' }
if (hasExpectedContent(editor.textContent)) return { success: true, method: 'contenteditable' }
}
return {
+48 -8
View File
@@ -78,6 +78,35 @@ function inspectSohuTaskState(rawUrl, publicUrl, draftSaved = false) {
function fillSohuContent(title, html, plainText) {
const sleep = milliseconds => new Promise(resolve => setTimeout(resolve, milliseconds))
const normalizeText = value => String(value || '').replace(/\s+/g, '')
const normalizeComparableText = value => String(value || '')
.normalize('NFKC')
.replace(/[^\p{L}\p{N}]+/gu, '')
function htmlToComparableText(value) {
const entities = {
'&amp;': '&',
'&gt;': '>',
'&lt;': '<',
'&quot;': '"',
'&#39;': "'",
'&nbsp;': ' ',
}
return normalizeComparableText(String(value || '')
.replace(/<img\b[^>]*>/gi, '')
.replace(/<[^>]+>/g, ' ')
.replace(/&(?:amp|gt|lt|quot|#39|nbsp);/g, entity => entities[entity] || entity))
}
const expectedText = htmlToComparableText(html)
const expectedImageCount = Array.from(String(html || '').matchAll(/<img\b[^>]*>/gi)).length
const minimumTextLength = Math.floor(expectedText.length * 0.8)
const sampleLength = Math.min(24, Math.max(6, Math.floor(expectedText.length / 4)))
const sampleStarts = expectedText.length > sampleLength
? [0, Math.floor((expectedText.length - sampleLength) / 2), expectedText.length - sampleLength]
: [0]
const expectedSamples = [...new Set(sampleStarts.map(start => (
expectedText.slice(start, start + sampleLength)
)).filter(Boolean))]
function waitForCondition(getValue, timeout = 15000) {
return new Promise(resolve => {
@@ -154,10 +183,14 @@ function fillSohuContent(title, html, plainText) {
})
}
function hasExpectedContent(text, imageCount, expectedPlainText) {
const expectedTextLength = normalizeText(expectedPlainText).length
const minimumLength = Math.min(Math.max(12, Math.floor(expectedTextLength * 0.1)), 120)
return normalizeText(text).length + imageCount * 12 >= minimumLength
function hasExpectedContent(text, imageCount) {
const editorText = normalizeComparableText(text)
const receivedText = expectedText.length === 0 || (
editorText.length >= minimumTextLength
&& expectedSamples.every(sample => editorText.includes(sample))
)
const receivedImages = expectedImageCount === 0 || imageCount >= expectedImageCount
return receivedText && receivedImages
}
function setEditableHtml(editor, nextHtml, nextPlainText) {
@@ -185,7 +218,14 @@ function fillSohuContent(title, html, plainText) {
].flatMap(selector => Array.from(document.querySelectorAll(selector))).find(Boolean)
})
if (!titleInput) return { success: false, error: '未找到搜狐号标题输入框' }
if (title) setTitleValue(titleInput, title)
if (title) {
setTitleValue(titleInput, title)
await sleep(200)
const receivedTitle = titleInput.isContentEditable ? titleInput.textContent : titleInput.value
if (normalizeText(receivedTitle) !== normalizeText(title)) {
return { success: false, error: '搜狐号未确认接收标题' }
}
}
if (!html) return { success: true, method: 'title-only' }
@@ -199,7 +239,7 @@ function fillSohuContent(title, html, plainText) {
const serialized = ueditor.getContent?.() || ''
const text = ueditor.getContentTxt?.() || serialized.replace(/<[^>]*>/g, '')
const imageCount = (serialized.match(/<img\b/gi) || []).length
if (hasExpectedContent(text, imageCount, plainText)) {
if (hasExpectedContent(text, imageCount)) {
return { success: true, method: 'ueditor', imageCount }
}
} catch {}
@@ -211,7 +251,7 @@ function fillSohuContent(title, html, plainText) {
setEditableHtml(iframeBody, html, plainText)
await sleep(500)
const imageCount = iframeBody.querySelectorAll('img').length
if (hasExpectedContent(iframeBody.innerText || iframeBody.textContent, imageCount, plainText)) {
if (hasExpectedContent(iframeBody.innerText || iframeBody.textContent, imageCount)) {
return { success: true, method: 'iframe', imageCount }
}
}
@@ -221,7 +261,7 @@ function fillSohuContent(title, html, plainText) {
setEditableHtml(contentEditor, html, plainText)
await sleep(500)
const imageCount = contentEditor.querySelectorAll('img').length
if (hasExpectedContent(contentEditor.innerText || contentEditor.textContent, imageCount, plainText)) {
if (hasExpectedContent(contentEditor.innerText || contentEditor.textContent, imageCount)) {
return { success: true, method: 'contenteditable', imageCount }
}
}
@@ -9,10 +9,6 @@ const TencentCloudPlatform = {
autoFillSupported: true,
}
function normalizeText(value) {
return String(value || '').replace(/\s+/g, '')
}
function normalizeString(value) {
if (typeof value === 'number') return String(value)
if (typeof value !== 'string') return undefined
@@ -99,8 +95,41 @@ function inspectTencentCloudTaskState(rawUrl, publicUrl, draftSaved = false) {
function fillTencentCloudContent(title, markdown, html, plainText) {
const contentToFill = markdown || plainText || ''
const normalizeText = value => String(value || '').replace(/\s+/g, '')
const normalizeContent = value => String(value || '').replace(/\r\n?/g, '\n')
const normalizeComparableText = value => String(value || '')
.normalize('NFKC')
.replace(/[^\p{L}\p{N}]+/gu, '')
const sleep = milliseconds => new Promise(resolve => setTimeout(resolve, milliseconds))
function htmlToComparableText(value) {
const entities = {
'&amp;': '&',
'&gt;': '>',
'&lt;': '<',
'&quot;': '"',
'&#39;': "'",
'&nbsp;': ' ',
}
return normalizeComparableText(String(value || '')
.replace(/<img\b[^>]*>/gi, '')
.replace(/<[^>]+>/g, ' ')
.replace(/&(?:amp|gt|lt|quot|#39|nbsp);/g, entity => entities[entity] || entity))
}
const expectedRichText = html
? htmlToComparableText(html)
: normalizeComparableText(plainText || contentToFill)
const expectedImageCount = Array.from(String(html || '').matchAll(/<img\b[^>]*>/gi)).length
const minimumTextLength = Math.floor(expectedRichText.length * 0.8)
const sampleLength = Math.min(24, Math.max(6, Math.floor(expectedRichText.length / 4)))
const sampleStarts = expectedRichText.length > sampleLength
? [0, Math.floor((expectedRichText.length - sampleLength) / 2), expectedRichText.length - sampleLength]
: [0]
const expectedSamples = [...new Set(sampleStarts.map(start => (
expectedRichText.slice(start, start + sampleLength)
)).filter(Boolean))]
function findFirst(selectors) {
return selectors.map(selector => document.querySelector(selector)).find(Boolean)
}
@@ -155,18 +184,24 @@ function fillTencentCloudContent(title, markdown, html, plainText) {
function getRichEditorSnapshot(editor) {
return {
textLength: normalizeText(editor?.innerText || editor?.textContent).length,
text: normalizeComparableText(editor?.innerText || editor?.textContent),
imageCount: editor?.querySelectorAll?.('img').length || 0,
}
}
function hasExpectedTitle(input) {
const expectedTitle = normalizeText(title)
return expectedTitle.length === 0 || normalizeText(input?.value) === expectedTitle
}
function hasExpectedRichText(editor) {
const expectedTextLength = normalizeText(plainText || contentToFill).length
const expectedImageCount = (String(html || '').match(/<img\b/gi) || []).length
const minimumTextLength = Math.min(Math.max(12, Math.floor(expectedTextLength * 0.1)), 120)
const snapshot = getRichEditorSnapshot(editor)
return (expectedTextLength === 0 || snapshot.textLength >= minimumTextLength)
&& snapshot.imageCount >= expectedImageCount
const receivedText = expectedRichText.length === 0 || (
snapshot.text.length >= minimumTextLength
&& expectedSamples.every(sample => snapshot.text.includes(sample))
)
const receivedImages = expectedImageCount === 0 || snapshot.imageCount >= expectedImageCount
return receivedText && receivedImages
}
function pasteRichText(editor) {
@@ -224,7 +259,13 @@ function fillTencentCloudContent(title, markdown, html, plainText) {
'.article-title input',
])
if (!titleInput) return { success: false, error: '未找到腾讯云开发者社区标题输入框' }
if (title) setInputValue(titleInput, title)
if (title) {
setInputValue(titleInput, title)
await sleep(200)
if (!hasExpectedTitle(titleInput)) {
return { success: false, error: '腾讯云开发者社区未确认接收标题' }
}
}
if (!contentToFill) return { success: true, method: 'title-only' }
@@ -258,7 +299,7 @@ function fillTencentCloudContent(title, markdown, html, plainText) {
editor.focus()
editor.setValue(contentToFill)
await sleep(250)
if (normalizeText(editor.getValue?.()) === normalizeText(contentToFill)) {
if (normalizeContent(editor.getValue?.()) === normalizeContent(contentToFill)) {
return { success: true, method: 'codemirror' }
}
return { success: false, error: '腾讯云开发者社区未确认接收正文' }
@@ -273,7 +314,7 @@ function fillTencentCloudContent(title, markdown, html, plainText) {
setInputValue(textarea, contentToFill)
await sleep(250)
if (normalizeText(textarea.value) !== normalizeText(contentToFill)) {
if (normalizeContent(textarea.value) !== normalizeContent(contentToFill)) {
return { success: false, error: '腾讯云开发者社区未确认接收正文' }
}
+76 -24
View File
@@ -236,25 +236,53 @@ function renderToutiaoPlainText(markdown) {
.replace(/\s+/g, '')
}
function fillToutiaoContent(title, markdown, body) {
const contentToFill = markdown || body || ''
function fillToutiaoContent(title, html, plainText) {
const contentHtml = html || ''
const contentText = plainText || contentHtml.replace(/<[^>]+>/g, ' ')
const sleep = milliseconds => new Promise(resolve => setTimeout(resolve, milliseconds))
const normalizeText = value => String(value || '').replace(/\s+/g, '')
const normalizeComparableText = value => String(value || '')
.normalize('NFKC')
.replace(/[^\p{L}\p{N}]+/gu, '')
function findFirst(selectors) {
return selectors.map(selector => document.querySelector(selector)).find(Boolean)
function htmlToComparableText(value) {
const entities = {
'&amp;': '&',
'&gt;': '>',
'&lt;': '<',
'&quot;': '"',
'&#39;': "'",
'&nbsp;': ' ',
}
return normalizeComparableText(String(value || '')
.replace(/<img\b[^>]*>/gi, '')
.replace(/<[^>]+>/g, ' ')
.replace(/&(?:amp|gt|lt|quot|#39|nbsp);/g, entity => entities[entity] || entity))
}
function waitForElement(selectors, timeout = 15000) {
function findFirst(selectors, excludedElement) {
for (const selector of selectors) {
const element = Array.from(document.querySelectorAll(selector))
.find(candidate => (
candidate !== excludedElement
&& !candidate.disabled
&& candidate.offsetParent !== null
))
if (element) return element
}
return null
}
function waitForElement(selectors, excludedElement, timeout = 15000) {
return new Promise(resolve => {
const existing = findFirst(selectors)
const existing = findFirst(selectors, excludedElement)
if (existing) {
resolve(existing)
return
}
const observer = new MutationObserver(() => {
const element = findFirst(selectors)
const element = findFirst(selectors, excludedElement)
if (!element) return
observer.disconnect()
resolve(element)
@@ -262,7 +290,7 @@ function fillToutiaoContent(title, markdown, body) {
observer.observe(document.documentElement, { childList: true, subtree: true })
window.setTimeout(() => {
observer.disconnect()
resolve(findFirst(selectors))
resolve(findFirst(selectors, excludedElement))
}, timeout)
})
}
@@ -312,39 +340,58 @@ function fillToutiaoContent(title, markdown, body) {
'input[placeholder*="标题"]',
])
if (!titleInput) return { success: false, error: '未找到今日头条标题输入框' }
if (title) setInputValue(titleInput, title)
if (title) {
setInputValue(titleInput, title)
await sleep(200)
if (normalizeText(titleInput.value) !== normalizeText(title)) {
return { success: false, error: '今日头条未确认接收标题' }
}
}
if (!contentHtml) return { success: true, method: 'title-only' }
const editor = await waitForElement([
'.ProseMirror[contenteditable="true"]',
'.ProseMirror',
'[contenteditable="true"][role="textbox"]',
])
], titleInput)
if (!editor) return { success: false, error: '未找到今日头条正文编辑器' }
if (!contentToFill) return { success: true, method: 'title-only' }
const html = renderToutiaoMarkdown(contentToFill)
const plainText = renderToutiaoPlainText(contentToFill)
dispatchHtmlPaste(editor, html, plainText)
editor.dispatchEvent(new InputEvent('input', { bubbles: true, inputType: 'insertFromPaste', data: plainText }))
dispatchHtmlPaste(editor, contentHtml, contentText)
editor.dispatchEvent(new InputEvent('input', { bubbles: true, inputType: 'insertFromPaste', data: contentText }))
editor.dispatchEvent(new Event('change', { bubbles: true }))
await sleep(700)
const expectedTextLength = plainText.length
const minimumLength = Math.min(Math.max(12, Math.floor(expectedTextLength * 0.1)), 120)
let editorTextLength = normalizeText(editor.innerText || editor.textContent).length
const expectedText = htmlToComparableText(contentHtml)
const expectedImageCount = Array.from(contentHtml.matchAll(/<img\b[^>]*>/gi)).length
const minimumTextLength = Math.floor(expectedText.length * 0.8)
const sampleLength = Math.min(24, Math.max(6, Math.floor(expectedText.length / 4)))
const sampleStarts = expectedText.length > sampleLength
? [0, Math.floor((expectedText.length - sampleLength) / 2), expectedText.length - sampleLength]
: [0]
const expectedSamples = [...new Set(sampleStarts.map(start => (
expectedText.slice(start, start + sampleLength)
)).filter(Boolean))]
let editorText = normalizeComparableText(editor.innerText || editor.textContent)
let imageCount = editor.querySelectorAll('img').length
const hasExpectedContent = () => (
(expectedText.length === 0 || (
editorText.length >= minimumTextLength
&& expectedSamples.every(sample => editorText.includes(sample))
))
&& (expectedImageCount === 0 || imageCount >= expectedImageCount)
)
if (editorTextLength + imageCount * 12 < minimumLength && typeof document.execCommand === 'function') {
if (!hasExpectedContent() && typeof document.execCommand === 'function') {
editor.focus()
selectEditorContents(editor)
document.execCommand('insertHTML', false, html)
editor.dispatchEvent(new InputEvent('input', { bubbles: true, inputType: 'insertFromPaste', data: plainText }))
document.execCommand('insertHTML', false, contentHtml)
editor.dispatchEvent(new InputEvent('input', { bubbles: true, inputType: 'insertFromPaste', data: contentText }))
await sleep(500)
editorTextLength = normalizeText(editor.innerText || editor.textContent).length
editorText = normalizeComparableText(editor.innerText || editor.textContent)
imageCount = editor.querySelectorAll('img').length
}
if (editorTextLength + imageCount * 12 < minimumLength) {
if (!hasExpectedContent()) {
return {
success: false,
error: '今日头条未确认接收正文,请在编辑器中手动粘贴后再重试',
@@ -407,10 +454,15 @@ async function syncToutiaoContent(tab, content, helpers) {
if (!tab?.id) return { success: false, message: '无法获取今日头条编辑器标签页' }
await helpers.waitForTab(tab.id)
const markdown = content.markdown || content.body || ''
const result = await helpers.chrome.scripting.executeScript({
target: { tabId: tab.id },
func: fillToutiaoContent,
args: [content.title, content.markdown, content.body],
args: [
content.title,
renderToutiaoMarkdown(markdown),
renderToutiaoPlainText(markdown),
],
world: 'MAIN',
})
const fillResult = result?.[0]?.result
+47 -8
View File
@@ -78,6 +78,35 @@ function inspectWangyihaoTaskState(rawUrl, publicUrl, draftSaved = false) {
function fillWangyihaoContent(title, html, plainText) {
const sleep = milliseconds => new Promise(resolve => setTimeout(resolve, milliseconds))
const normalizeText = value => String(value || '').replace(/\s+/g, '')
const normalizeComparableText = value => String(value || '')
.normalize('NFKC')
.replace(/[^\p{L}\p{N}]+/gu, '')
function htmlToComparableText(value) {
const entities = {
'&amp;': '&',
'&gt;': '>',
'&lt;': '<',
'&quot;': '"',
'&#39;': "'",
'&nbsp;': ' ',
}
return normalizeComparableText(String(value || '')
.replace(/<img\b[^>]*>/gi, '')
.replace(/<[^>]+>/g, ' ')
.replace(/&(?:amp|gt|lt|quot|#39|nbsp);/g, entity => entities[entity] || entity))
}
const expectedText = htmlToComparableText(html)
const expectedImageCount = Array.from(String(html || '').matchAll(/<img\b[^>]*>/gi)).length
const minimumTextLength = Math.floor(expectedText.length * 0.8)
const sampleLength = Math.min(24, Math.max(6, Math.floor(expectedText.length / 4)))
const sampleStarts = expectedText.length > sampleLength
? [0, Math.floor((expectedText.length - sampleLength) / 2), expectedText.length - sampleLength]
: [0]
const expectedSamples = [...new Set(sampleStarts.map(start => (
expectedText.slice(start, start + sampleLength)
)).filter(Boolean))]
function findFirst(selectors, exclude) {
return selectors
@@ -156,12 +185,15 @@ function fillWangyihaoContent(title, html, plainText) {
return true
}
function hasExpectedContent(editor, expectedPlainText) {
const expectedTextLength = normalizeText(expectedPlainText).length
const minimumLength = Math.min(Math.max(12, Math.floor(expectedTextLength * 0.1)), 120)
const editorTextLength = normalizeText(editor.innerText || editor.textContent).length
function hasExpectedContent(editor) {
const editorText = normalizeComparableText(editor.innerText || editor.textContent)
const imageCount = editor.querySelectorAll('img').length
return editorTextLength + imageCount * 12 >= minimumLength
const receivedText = expectedText.length === 0 || (
editorText.length >= minimumTextLength
&& expectedSamples.every(sample => editorText.includes(sample))
)
const receivedImages = expectedImageCount === 0 || imageCount >= expectedImageCount
return receivedText && receivedImages
}
async function fill() {
@@ -172,7 +204,14 @@ function fillWangyihaoContent(title, html, plainText) {
'[contenteditable="true"][placeholder*="标题"]',
])
if (!titleInput) return { success: false, error: '未找到网易号标题输入框' }
if (title) setTitleValue(titleInput, title)
if (title) {
setTitleValue(titleInput, title)
await sleep(200)
const receivedTitle = titleInput.isContentEditable ? titleInput.textContent : titleInput.value
if (normalizeText(receivedTitle) !== normalizeText(title)) {
return { success: false, error: '网易号未确认接收标题' }
}
}
if (!html) return { success: true, method: 'title-only' }
@@ -194,7 +233,7 @@ function fillWangyihaoContent(title, html, plainText) {
editor.dispatchEvent(new Event('change', { bubbles: true }))
await sleep(700)
if (!hasExpectedContent(editor, plainText) && typeof document.execCommand === 'function') {
if (!hasExpectedContent(editor) && typeof document.execCommand === 'function') {
editor.focus()
selectEditorContents(editor)
document.execCommand('insertHTML', false, html)
@@ -206,7 +245,7 @@ function fillWangyihaoContent(title, html, plainText) {
await sleep(500)
}
if (!hasExpectedContent(editor, plainText)) {
if (!hasExpectedContent(editor)) {
return {
success: false,
error: '网易号未确认接收正文,请在编辑器中手动粘贴后再重试',
+52 -17
View File
@@ -81,22 +81,41 @@ function fillZhihuContent(title, markdown, body) {
const contentToFill = markdown || body || ''
const sleep = milliseconds => new Promise(resolve => setTimeout(resolve, milliseconds))
const normalizeText = value => (value || '').replace(/\s+/g, '')
const normalizeText = value => String(value || '').replace(/\s+/g, '')
const normalizeComparableText = value => String(value || '')
.normalize('NFKC')
.replace(/[^\p{L}\p{N}]+/gu, '')
function findFirst(selectors) {
return selectors.map(selector => document.querySelector(selector)).find(Boolean)
function markdownToComparableText(value) {
return normalizeComparableText(String(value || '')
.replace(/!\[[^\]]*]\([^)]*\)/g, '')
.replace(/\[([^\]]+)]\([^)]*\)/g, '$1')
.replace(/<[^>]+>/g, ''))
}
function waitForElement(selectors, timeout = 15000) {
function findFirst(selectors, excludedElement) {
for (const selector of selectors) {
const element = Array.from(document.querySelectorAll(selector))
.find(candidate => (
candidate !== excludedElement
&& !candidate.disabled
&& candidate.offsetParent !== null
))
if (element) return element
}
return null
}
function waitForElement(selectors, excludedElement, timeout = 15000) {
return new Promise(resolve => {
const existing = findFirst(selectors)
const existing = findFirst(selectors, excludedElement)
if (existing) {
resolve(existing)
return
}
const observer = new MutationObserver(() => {
const element = findFirst(selectors)
const element = findFirst(selectors, excludedElement)
if (!element) return
observer.disconnect()
@@ -105,7 +124,7 @@ function fillZhihuContent(title, markdown, body) {
observer.observe(document.documentElement, { childList: true, subtree: true })
window.setTimeout(() => {
observer.disconnect()
resolve(findFirst(selectors))
resolve(findFirst(selectors, excludedElement))
}, timeout)
})
}
@@ -169,7 +188,13 @@ function fillZhihuContent(title, markdown, body) {
if (!titleInput) {
return { success: false, error: '未找到知乎标题输入框' }
}
if (title) setInputValue(titleInput, title)
if (title) {
setInputValue(titleInput, title)
await sleep(200)
if (normalizeText(titleInput.value) !== normalizeText(title)) {
return { success: false, error: '知乎未确认接收标题' }
}
}
const editor = await waitForElement([
'.public-DraftEditor-content[contenteditable="true"]',
@@ -177,7 +202,7 @@ function fillZhihuContent(title, markdown, body) {
'.DraftEditor-root [contenteditable="true"]',
'.WriteIndex-content [contenteditable="true"]',
'[contenteditable="true"][role="textbox"]',
])
], titleInput)
if (!editor) {
return { success: false, error: '未找到知乎正文编辑器' }
}
@@ -203,14 +228,24 @@ function fillZhihuContent(title, markdown, body) {
const parsed = await clickMarkdownParser()
await sleep(parsed ? 1000 : 600)
const expectedTextLength = normalizeText(contentToFill
.replace(/!\[[^\]]*]\([^)]*\)/g, '')
.replace(/[\[\]#*_>`~()]/g, ''))
.length
const editorTextLength = normalizeText(editor.innerText || editor.textContent).length
const minimumLength = Math.min(Math.max(12, Math.floor(expectedTextLength * 0.1)), 120)
const imageEquivalentLength = editor.querySelectorAll('img').length * 12
if (editorTextLength + imageEquivalentLength < minimumLength) {
const expectedText = markdownToComparableText(contentToFill)
const editorText = normalizeComparableText(editor.innerText || editor.textContent)
const expectedImageCount = Array.from(contentToFill.matchAll(/!\[[^\]]*]\([^)]*\)/g)).length
const editorImageCount = editor.querySelectorAll('img').length
const minimumTextLength = Math.floor(expectedText.length * 0.8)
const sampleLength = Math.min(24, Math.max(6, Math.floor(expectedText.length / 4)))
const sampleStarts = expectedText.length > sampleLength
? [0, Math.floor((expectedText.length - sampleLength) / 2), expectedText.length - sampleLength]
: [0]
const expectedSamples = [...new Set(sampleStarts.map(start => (
expectedText.slice(start, start + sampleLength)
)).filter(Boolean))]
const receivedText = expectedText.length === 0 || (
editorText.length >= minimumTextLength
&& expectedSamples.every(sample => editorText.includes(sample))
)
const receivedImages = expectedImageCount === 0 || editorImageCount >= expectedImageCount
if (!receivedText || !receivedImages) {
return {
success: false,
error: '知乎未确认接收正文,请在编辑器中手动粘贴后再重试',
+1 -1
View File
@@ -1,7 +1,7 @@
{
"manifest_version": 3,
"name": "AiToEarn - 内容营销助手",
"version": "1.19.2",
"version": "1.19.6",
"description": "AiToEarn 自维护内容扩展:网页采集、账号检测、多平台文章分发与受控人工互动",
"permissions": [
"activeTab",
+86
View File
@@ -242,6 +242,92 @@ assert.deepEqual(
juejinProviderSnapshot,
)
const originalQiehaoMessageHandler = globalThis.chrome.runtime.sendMessage
const originalQiehaoScriptHandler = globalThis.chrome.scripting.executeScript
const qiehaoClickedActions = []
const qiehaoActionArguments = []
globalThis.chrome.runtime.sendMessage = async (message) => {
sentMessages.push(message)
if (message.type === 'GET_PLATFORM_ACCOUNT') {
return {
success: true,
platformUid: '24973524',
displayName: '企鹅号创作者973524',
}
}
if (message.type === 'OPEN_PLATFORM_TASK') {
return {
success: true,
status: 'filled',
tabId: 202,
url: 'https://om.qq.com/main/creation/article',
message: '企鹅号编辑器已填充',
}
}
if (message.type === 'INSPECT_PLATFORM_TASK') {
return {
success: true,
status: 'published',
url: 'https://page.om.qq.com/page/ABCDEF123.html',
message: '已确认企鹅号文章公开地址',
}
}
throw new Error(`Unexpected message: ${message.type}`)
}
globalThis.chrome.scripting.executeScript = async ({ func, args }) => {
scriptExecutionCount += 1
qiehaoActionArguments.push(args)
const buttons = [
{
textContent: '定时发布',
disabled: false,
getAttribute: () => null,
click: () => qiehaoClickedActions.push('定时发布'),
},
{
textContent: '发布',
disabled: false,
getAttribute: () => null,
click: () => qiehaoClickedActions.push('发布'),
},
]
globalThis.document = {
querySelectorAll: (selector) => {
assert.equal(selector, 'button.omui-button.omui-button--primary')
return buttons
},
}
try {
return [{ result: func(...args) }]
}
finally {
delete globalThis.document
}
}
sentMessages.length = 0
const qiehaoPublishExecution = await executeClaimedTask({
platform: 'qiehao',
platformUid: '24973524',
title: '企鹅号精确按钮验证',
body: '<p>正文</p>',
handoff: {
publishMode: 'publish',
expiresAt: new Date(Date.now() + 60_000).toISOString(),
},
})
assert.equal(qiehaoPublishExecution.report, true)
assert.equal(qiehaoPublishExecution.result.status, 'published')
assert.equal(qiehaoPublishExecution.result.platformUid, '24973524')
assert.deepEqual(qiehaoActionArguments, [[
'button.omui-button.omui-button--primary',
'发布',
]])
assert.deepEqual(qiehaoClickedActions, ['发布'])
globalThis.chrome.runtime.sendMessage = originalQiehaoMessageHandler
globalThis.chrome.scripting.executeScript = originalQiehaoScriptHandler
delete globalThis.chrome
const markdownTaskContent = toTaskContent({
+748 -6
View File
@@ -1,5 +1,6 @@
import assert from 'node:assert/strict'
import { readFile } from 'node:fs/promises'
import { runInNewContext } from 'node:vm'
import {
BaijiahaoLoginConfig,
ToutiaoLoginConfig,
@@ -9,58 +10,85 @@ import { parseCsdnAccount } from '../distribution/cose/detection/src/platforms/c
import { parseSegmentFaultAccount } from '../distribution/cose/detection/src/platforms/segmentfault.js'
import {
CSDNPlatform,
fillCsdnContent,
inspectCsdnTaskState,
parseCsdnPublishedUrl,
} from '../distribution/cose/core/platforms/csdn.js'
import {
fillSegmentFaultContent,
inspectSegmentFaultTaskState,
parseSegmentFaultPublishedUrl,
SegmentFaultPlatform,
} from '../distribution/cose/core/platforms/segmentfault.js'
import { parseJianshuAccount } from '../distribution/cose/detection/src/platforms/jianshu.js'
import { inspectJianshuTaskState } from '../distribution/cose/core/platforms/jianshu.js'
import { inspectJuejinTaskState } from '../distribution/cose/core/platforms/juejin.js'
import { inspectZhihuTaskState } from '../distribution/cose/core/platforms/zhihu.js'
import {
fillJianshuContent,
inspectJianshuTaskState,
} from '../distribution/cose/core/platforms/jianshu.js'
import {
fillJuejinContent,
inspectJuejinTaskState,
} from '../distribution/cose/core/platforms/juejin.js'
import {
fillZhihuContent,
inspectZhihuTaskState,
} from '../distribution/cose/core/platforms/zhihu.js'
import {
fillToutiaoContent,
inspectToutiaoTaskState,
renderToutiaoMarkdown,
} from '../distribution/cose/core/platforms/toutiao.js'
import { inspectBaijiahaoTaskState } from '../distribution/cose/core/platforms/baijiahao.js'
import {
fillBaijiahaoContent,
inspectBaijiahaoTaskState,
} from '../distribution/cose/core/platforms/baijiahao.js'
import { parseWangyihaoAccount } from '../distribution/cose/detection/src/platforms/wangyihao.js'
import { inspectWangyihaoTaskState } from '../distribution/cose/core/platforms/wangyihao.js'
import {
fillWangyihaoContent,
inspectWangyihaoTaskState,
} from '../distribution/cose/core/platforms/wangyihao.js'
import { parseSohuAccount } from '../distribution/cose/detection/src/platforms/sohu.js'
import { inspectSohuTaskState } from '../distribution/cose/core/platforms/sohu.js'
import {
fillSohuContent,
inspectSohuTaskState,
} from '../distribution/cose/core/platforms/sohu.js'
import { parseAliyunAccount } from '../distribution/cose/detection/src/platforms/aliyun.js'
import {
AliyunPlatform,
fillAliyunContent,
inspectAliyunTaskState,
parseAliyunPublishedUrl,
} from '../distribution/cose/core/platforms/aliyun.js'
import { parseCto51Account } from '../distribution/cose/detection/src/platforms/cto51.js'
import {
CTO51Platform,
fillCto51Content,
inspectCto51TaskState,
parseCto51PublishedUrl,
} from '../distribution/cose/core/platforms/cto51.js'
import { parseOSChinaAccount } from '../distribution/cose/detection/src/platforms/oschina.js'
import {
fillOSChinaContent,
inspectOSChinaTaskState,
OSChinaPlatform,
} from '../distribution/cose/core/platforms/oschina.js'
import { parseQiehaoAccount } from '../distribution/cose/detection/src/platforms/qiehao.js'
import {
fillQiehaoContent,
inspectQiehaoTaskState,
parseQiehaoPublishedUrl,
QiehaoPlatform,
} from '../distribution/cose/core/platforms/qiehao.js'
import { parseTencentCloudAccount } from '../distribution/cose/detection/src/platforms/tencentcloud.js'
import {
fillTencentCloudContent,
inspectTencentCloudTaskState,
parseTencentCloudPublishedUrl,
TencentCloudPlatform,
} from '../distribution/cose/core/platforms/tencentcloud.js'
import { parseInfoQAccount } from '../distribution/cose/detection/src/platforms/infoq.js'
import {
fillInfoQContent,
inspectInfoQTaskState,
parseInfoQDraftCreation,
} from '../distribution/cose/core/platforms/infoq.js'
@@ -72,6 +100,239 @@ import {
} from '../distribution/cose/core/platforms/index.js'
import { canReturnExistingPlatformTaskState } from '../distribution/cose/core/task-state.js'
async function runSerializedTitleFill(fillFunction, args, options = {}) {
const truncateTitle = options.truncateTitle === true
class FakeEvent {
constructor(type, init = {}) {
this.type = type
Object.assign(this, init)
}
}
class FakeInputElement {
constructor(truncate = truncateTitle, stripWhitespace = false) {
this.disabled = false
this.offsetParent = {}
this.storedValue = ''
this.truncate = truncate
this.stripWhitespace = stripWhitespace
}
get value() {
return this.storedValue
}
set value(nextValue) {
const normalized = String(nextValue || '')
this.storedValue = this.truncate
? normalized.slice(0, 1)
: this.stripWhitespace
? normalized.replace(/\s+/g, '')
: normalized
}
focus() {}
contains() {
return false
}
dispatchEvent() {
return true
}
}
class FakeTextAreaElement extends FakeInputElement {}
function createContentEditable({ truncate = false } = {}) {
return {
disabled: false,
isContentEditable: true,
offsetParent: {},
storedHtml: '',
storedText: '',
contains() {
return false
},
focus() {},
dispatchEvent(event) {
if (event?.type === 'paste' && event.clipboardData) {
this.innerHTML = event.clipboardData.getData('text/html')
const plainText = event.clipboardData.getData('text/plain')
this.textContent = plainText
}
return true
},
querySelectorAll() {
return []
},
get innerHTML() {
return this.storedHtml
},
set innerHTML(nextHtml) {
this.storedHtml = String(nextHtml || '')
const normalized = this.storedHtml.replace(/<[^>]+>/g, '').replace(/&lt;/g, '<')
this.textContent = normalized
},
get textContent() {
return this.storedText
},
set textContent(nextText) {
const normalized = String(nextText || '')
this.storedText = truncate ? normalized.slice(0, 1) : normalized
},
}
}
class FakeDataTransfer {
constructor() {
this.values = new Map()
}
setData(type, value) {
this.values.set(type, String(value || ''))
}
getData(type) {
return this.values.get(type) || ''
}
}
const input = new FakeInputElement()
const bodyTextarea = new FakeTextAreaElement(
options.truncateBody === true,
options.stripBodyWhitespace === true,
)
const contentEditable = createContentEditable({ truncate: truncateTitle })
const bodyEditor = createContentEditable({ truncate: options.truncateBody === true })
const titleElement = options.contentEditable ? contentEditable : input
const markdownParserButton = {
disabled: false,
offsetParent: {},
textContent: '确认并解析',
click() {},
}
const selection = {
removeAllRanges() {},
addRange() {},
}
const document = {
documentElement: {},
defaultView: {
getSelection() {
return selection
},
},
querySelector(selector) {
const isBodyTextarea = /bytemd-(?:body|editor)|arthur-editor|_3swFR|article-editor|textarea\.textarea|markdown-editor|editor-container|editormd-markdown-textarea|textarea\[name="content"\]|textarea\[placeholder\*="正文"\]/.test(selector)
if ((options.bodyTextarea || options.bodyContentEditable) && selector === '.CodeMirror') return null
if (options.bodyTextarea && /ProseMirror|role="textbox"|cm-content|editor__inner|markdown-highlighting/.test(selector)) return null
if (options.bodyContentEditable && /ProseMirror|role="textbox"|cm-content|editor__inner|markdown-highlighting/.test(selector)) {
return bodyEditor
}
if (options.bodyTextarea && isBodyTextarea) {
return options.missingBody ? null : bodyTextarea
}
if (selector === '.editor-switch-btn') return null
if (options.missingTitle) return null
return titleElement
},
querySelectorAll(selector) {
const isBodyTextarea = /bytemd-(?:body|editor)|arthur-editor|_3swFR|article-editor|textarea\.textarea|markdown-editor|editor-container|editormd-markdown-textarea|textarea\[name="content"\]|textarea\[placeholder\*="正文"\]/.test(selector)
if ((options.bodyTextarea || options.bodyContentEditable) && selector === '.CodeMirror') return []
if (options.bodyTextarea && /ProseMirror|role="textbox"|cm-content|editor__inner|markdown-highlighting/.test(selector)) return []
if (selector === 'button' && options.markdownParserButton) return [markdownParserButton]
if (options.bodyContentEditable && selector === '[contenteditable="true"]') {
return [bodyEditor]
}
if (options.bodyContentEditable && selector === '[contenteditable="true"], .edui-body') {
return [bodyEditor]
}
if (options.bodyContentEditable && /ProseMirror|role="textbox"|cm-content|editor__inner|markdown-highlighting/.test(selector)) {
return [bodyEditor]
}
if (options.bodyTextarea && isBodyTextarea) {
return options.missingBody ? [] : [bodyTextarea]
}
if (options.missingTitle) return []
return [titleElement]
},
createRange() {
return { selectNodeContents() {} }
},
execCommand() {
return false
},
}
contentEditable.ownerDocument = document
bodyEditor.ownerDocument = document
const immediateTimeout = callback => {
callback()
return 1
}
const window = {
HTMLInputElement: FakeInputElement,
HTMLTextAreaElement: FakeTextAreaElement,
getSelection() {
return selection
},
setTimeout: immediateTimeout,
}
class FakeMutationObserver {
observe() {}
disconnect() {}
}
const isolatedFill = runInNewContext(`(${fillFunction.toString()})`, {
ClipboardEvent: FakeEvent,
DataTransfer: FakeDataTransfer,
document,
Event: FakeEvent,
HTMLInputElement: FakeInputElement,
HTMLTextAreaElement: FakeTextAreaElement,
InputEvent: FakeEvent,
MutationObserver: FakeMutationObserver,
setTimeout: immediateTimeout,
window,
})
const result = await isolatedFill(...args)
return {
bodyText: options.bodyTextarea ? bodyTextarea.value : bodyEditor.textContent,
result,
title: options.contentEditable ? contentEditable.textContent : input.value,
}
}
async function assertSerializedTitleFill({
fillFunction,
args,
bodyContentEditable = false,
contentEditable = false,
error,
expectedBodyText,
expectedMethod = 'title-only',
}) {
const expectedTitle = args[0]
const success = await runSerializedTitleFill(fillFunction, args, {
bodyContentEditable,
contentEditable,
})
assert.equal(success.result.success, true)
assert.equal(success.result.method, expectedMethod)
assert.equal(success.title, expectedTitle)
if (expectedBodyText !== undefined) assert.equal(success.bodyText, expectedBodyText)
const truncated = await runSerializedTitleFill(fillFunction, args, {
bodyContentEditable,
contentEditable,
truncateTitle: true,
})
assert.equal(truncated.result.success, false)
assert.equal(truncated.result.error, error)
}
assert.deepEqual(
inspectJuejinTaskState('https://juejin.cn/editor/drafts/new'),
{
@@ -119,6 +380,38 @@ assert.equal(
false,
)
await assertSerializedTitleFill({
fillFunction: fillJuejinContent,
args: ['掘金完整标题验证', '', ''],
error: '掘金未确认接收标题',
})
const juejinBodySuccess = await runSerializedTitleFill(
fillJuejinContent,
['掘金正文验证', '# 掘金正文\n\n- 列表项', ''],
{ bodyTextarea: true },
)
assert.equal(juejinBodySuccess.result.success, true)
assert.equal(juejinBodySuccess.result.method, 'textarea')
assert.equal(juejinBodySuccess.title, '掘金正文验证')
assert.equal(juejinBodySuccess.bodyText, '# 掘金正文\n\n- 列表项')
const juejinBodyTruncated = await runSerializedTitleFill(
fillJuejinContent,
['掘金正文验证', '# 掘金正文\n\n- 列表项', ''],
{ bodyTextarea: true, truncateBody: true },
)
assert.equal(juejinBodyTruncated.result.success, false)
assert.equal(juejinBodyTruncated.result.error, '掘金未确认接收正文')
const juejinBodyWhitespaceLost = await runSerializedTitleFill(
fillJuejinContent,
['掘金正文验证', '# 掘金正文\n\n- 列表项\n\n```js\nconst value = 1\n```', ''],
{ bodyTextarea: true, stripBodyWhitespace: true },
)
assert.equal(juejinBodyWhitespaceLost.result.success, false)
assert.equal(juejinBodyWhitespaceLost.result.error, '掘金未确认接收正文')
assert.deepEqual(
parseJianshuAccount({
data: {
@@ -167,6 +460,54 @@ assert.equal(
true,
)
await assertSerializedTitleFill({
fillFunction: fillJianshuContent,
args: ['简书完整标题验证', '', ''],
error: '简书未确认接收标题',
})
const jianshuMissingTitle = await runSerializedTitleFill(
fillJianshuContent,
['简书完整标题验证', '', ''],
{ missingTitle: true },
)
assert.equal(jianshuMissingTitle.result.success, false)
assert.equal(jianshuMissingTitle.result.error, '未找到简书标题输入框')
const jianshuBodySuccess = await runSerializedTitleFill(
fillJianshuContent,
['简书正文验证', '# 简书正文\n\n- 列表项', ''],
{ bodyTextarea: true },
)
assert.equal(jianshuBodySuccess.result.success, true)
assert.equal(jianshuBodySuccess.result.method, 'textarea')
assert.equal(jianshuBodySuccess.title, '简书正文验证')
assert.equal(jianshuBodySuccess.bodyText, '# 简书正文\n\n- 列表项')
const jianshuBodyTruncated = await runSerializedTitleFill(
fillJianshuContent,
['简书正文验证', '# 简书正文\n\n- 列表项', ''],
{ bodyTextarea: true, truncateBody: true },
)
assert.equal(jianshuBodyTruncated.result.success, false)
assert.equal(jianshuBodyTruncated.result.error, '简书未确认接收正文')
const jianshuBodyWhitespaceLost = await runSerializedTitleFill(
fillJianshuContent,
['简书正文验证', '# 简书正文\n\n- 列表项\n\n```js\nconst value = 1\n```', ''],
{ bodyTextarea: true, stripBodyWhitespace: true },
)
assert.equal(jianshuBodyWhitespaceLost.result.success, false)
assert.equal(jianshuBodyWhitespaceLost.result.error, '简书未确认接收正文')
const jianshuMissingBody = await runSerializedTitleFill(
fillJianshuContent,
['简书正文验证', '# 简书正文', ''],
{ bodyTextarea: true, missingBody: true },
)
assert.equal(jianshuMissingBody.result.success, false)
assert.equal(jianshuMissingBody.result.error, '未找到简书 Markdown 编辑器')
assert.deepEqual(
ZhihuLoginConfig.getUserInfo({
id: 'zhihu-user-1',
@@ -221,6 +562,38 @@ assert.equal(
true,
)
await assertSerializedTitleFill({
fillFunction: fillZhihuContent,
args: ['知乎完整标题验证', '', ''],
bodyContentEditable: true,
error: '知乎未确认接收标题',
})
const zhihuBodySuccess = await runSerializedTitleFill(
fillZhihuContent,
[
'知乎正文验证',
'# 知乎正文验证\n\n这是用于核对富文本转换完整性的第一段内容。\n\n- 中间列表项目\n\n这是必须保留的结尾内容。',
'',
],
{ bodyContentEditable: true, markdownParserButton: true },
)
assert.equal(zhihuBodySuccess.result.success, true)
assert.equal(zhihuBodySuccess.result.method, 'markdown-parse')
assert.equal(zhihuBodySuccess.title, '知乎正文验证')
const zhihuBodyTruncated = await runSerializedTitleFill(
fillZhihuContent,
[
'知乎正文验证',
'# 知乎正文验证\n\n这是用于核对富文本转换完整性的第一段内容。\n\n- 中间列表项目\n\n这是必须保留的结尾内容。',
'',
],
{ bodyContentEditable: true, markdownParserButton: true, truncateBody: true },
)
assert.equal(zhihuBodyTruncated.result.success, false)
assert.equal(zhihuBodyTruncated.result.error, '知乎未确认接收正文,请在编辑器中手动粘贴后再重试')
assert.deepEqual(
ToutiaoLoginConfig.getUserInfo({
code: 0,
@@ -341,6 +714,38 @@ assert.equal(
true,
)
await assertSerializedTitleFill({
fillFunction: fillBaijiahaoContent,
args: ['百家号完整标题验证', '', ''],
contentEditable: true,
error: '百家号未确认接收标题',
})
const baijiahaoBodySuccess = await runSerializedTitleFill(
fillBaijiahaoContent,
[
'百家号正文验证',
'<h1>百家号正文验证</h1><p>这是用于核对富文本转换完整性的第一段内容。</p><ul><li>中间列表项目</li></ul><p>这是必须保留的结尾内容。</p>',
'百家号正文验证这是用于核对富文本转换完整性的第一段内容。中间列表项目这是必须保留的结尾内容。',
],
{ contentEditable: true, bodyContentEditable: true },
)
assert.equal(baijiahaoBodySuccess.result.success, true)
assert.equal(baijiahaoBodySuccess.result.method, 'contenteditable')
assert.equal(baijiahaoBodySuccess.title, '百家号正文验证')
const baijiahaoBodyTruncated = await runSerializedTitleFill(
fillBaijiahaoContent,
[
'百家号正文验证',
'<h1>百家号正文验证</h1><p>这是用于核对富文本转换完整性的第一段内容。</p><ul><li>中间列表项目</li></ul><p>这是必须保留的结尾内容。</p>',
'百家号正文验证这是用于核对富文本转换完整性的第一段内容。中间列表项目这是必须保留的结尾内容。',
],
{ contentEditable: true, bodyContentEditable: true, truncateBody: true },
)
assert.equal(baijiahaoBodyTruncated.result.success, false)
assert.equal(baijiahaoBodyTruncated.result.error, '百家号未确认接收正文,请在编辑器中手动粘贴后再重试')
assert.deepEqual(
parseWangyihaoAccount({
code: 1,
@@ -399,6 +804,37 @@ assert.equal(
assert.equal(typeof SYNC_HANDLERS.wangyi, 'function')
assert.equal(typeof INSPECT_HANDLERS.wangyi, 'function')
await assertSerializedTitleFill({
fillFunction: fillWangyihaoContent,
args: ['网易号完整标题验证', '', ''],
error: '网易号未确认接收标题',
})
const wangyihaoBodySuccess = await runSerializedTitleFill(
fillWangyihaoContent,
[
'网易号正文验证',
'<h1>网易号正文验证</h1><p>这是用于核对富文本转换完整性的第一段内容。</p><ul><li>中间列表项目</li></ul><p>这是必须保留的结尾内容。</p>',
'网易号正文验证这是用于核对富文本转换完整性的第一段内容。中间列表项目这是必须保留的结尾内容。',
],
{ bodyContentEditable: true },
)
assert.equal(wangyihaoBodySuccess.result.success, true)
assert.equal(wangyihaoBodySuccess.result.method, 'paste-html')
assert.equal(wangyihaoBodySuccess.title, '网易号正文验证')
const wangyihaoBodyTruncated = await runSerializedTitleFill(
fillWangyihaoContent,
[
'网易号正文验证',
'<h1>网易号正文验证</h1><p>这是用于核对富文本转换完整性的第一段内容。</p><ul><li>中间列表项目</li></ul><p>这是必须保留的结尾内容。</p>',
'网易号正文验证这是用于核对富文本转换完整性的第一段内容。中间列表项目这是必须保留的结尾内容。',
],
{ bodyContentEditable: true, truncateBody: true },
)
assert.equal(wangyihaoBodyTruncated.result.success, false)
assert.equal(wangyihaoBodyTruncated.result.error, '网易号未确认接收正文,请在编辑器中手动粘贴后再重试')
const wangyihaoDetectorSource = await readFile(
new URL('../distribution/cose/detection/src/platforms/wangyihao.js', import.meta.url),
'utf8',
@@ -466,6 +902,37 @@ assert.equal(
assert.equal(typeof SYNC_HANDLERS.sohu, 'function')
assert.equal(typeof INSPECT_HANDLERS.sohu, 'function')
await assertSerializedTitleFill({
fillFunction: fillSohuContent,
args: ['搜狐号完整标题验证', '', ''],
error: '搜狐号未确认接收标题',
})
const sohuBodySuccess = await runSerializedTitleFill(
fillSohuContent,
[
'搜狐号正文验证',
'<h1>搜狐号正文验证</h1><p>这是用于核对富文本转换完整性的第一段内容。</p><ul><li>中间列表项目</li></ul><p>这是必须保留的结尾内容。</p>',
'搜狐号正文验证这是用于核对富文本转换完整性的第一段内容。中间列表项目这是必须保留的结尾内容。',
],
{ bodyContentEditable: true },
)
assert.equal(sohuBodySuccess.result.success, true)
assert.equal(sohuBodySuccess.result.method, 'contenteditable')
assert.equal(sohuBodySuccess.title, '搜狐号正文验证')
const sohuBodyTruncated = await runSerializedTitleFill(
fillSohuContent,
[
'搜狐号正文验证',
'<h1>搜狐号正文验证</h1><p>这是用于核对富文本转换完整性的第一段内容。</p><ul><li>中间列表项目</li></ul><p>这是必须保留的结尾内容。</p>',
'搜狐号正文验证这是用于核对富文本转换完整性的第一段内容。中间列表项目这是必须保留的结尾内容。',
],
{ bodyContentEditable: true, truncateBody: true },
)
assert.equal(sohuBodyTruncated.result.success, false)
assert.equal(sohuBodyTruncated.result.error, '搜狐号未确认接收正文,请在编辑器中手动粘贴后再重试')
const sohuDetectorSource = await readFile(
new URL('../distribution/cose/detection/src/platforms/sohu.js', import.meta.url),
'utf8',
@@ -788,6 +1255,194 @@ const qiehaoDetectorSource = await readFile(
)
assert.doesNotMatch(qiehaoDetectorSource, /chrome\.cookies|Cookie\s*:/)
await assertSerializedTitleFill({
fillFunction: fillQiehaoContent,
args: ['企鹅号完整标题验证', '', ''],
contentEditable: true,
error: '企鹅号未确认接收标题',
})
const qiehaoBodySuccess = await runSerializedTitleFill(
fillQiehaoContent,
[
'企鹅号正文验证',
'<h1>企鹅号正文验证</h1><p>这是用于核对富文本转换完整性的第一段内容。</p><ul><li>中间列表项目</li></ul><p>这是必须保留的结尾内容。</p>',
'企鹅号正文验证这是用于核对富文本转换完整性的第一段内容。中间列表项目这是必须保留的结尾内容。',
],
{ contentEditable: true, bodyContentEditable: true },
)
assert.equal(qiehaoBodySuccess.result.success, true)
assert.equal(qiehaoBodySuccess.result.method, 'rich-text')
assert.equal(qiehaoBodySuccess.title, '企鹅号正文验证')
const qiehaoBodyTruncated = await runSerializedTitleFill(
fillQiehaoContent,
[
'企鹅号正文验证',
'<h1>企鹅号正文验证</h1><p>这是用于核对富文本转换完整性的第一段内容。</p><ul><li>中间列表项目</li></ul><p>这是必须保留的结尾内容。</p>',
'企鹅号正文验证这是用于核对富文本转换完整性的第一段内容。中间列表项目这是必须保留的结尾内容。',
],
{ contentEditable: true, bodyContentEditable: true, truncateBody: true },
)
assert.equal(qiehaoBodyTruncated.result.success, false)
assert.equal(qiehaoBodyTruncated.result.error, '企鹅号未确认接收正文,请在编辑器中手动粘贴后再重试')
await assertSerializedTitleFill({
fillFunction: fillTencentCloudContent,
args: ['腾讯云完整标题验证', '', '', ''],
error: '腾讯云开发者社区未确认接收标题',
})
const tencentCloudBodySuccess = await runSerializedTitleFill(
fillTencentCloudContent,
[
'腾讯云正文验证',
'# 腾讯云正文验证\n\n- 中间列表项目\n\n这是必须保留的结尾内容。',
'<h1>腾讯云正文验证</h1><ul><li>中间列表项目</li></ul><p>这是必须保留的结尾内容。</p>',
'腾讯云正文验证中间列表项目这是必须保留的结尾内容。',
],
{ bodyContentEditable: true },
)
assert.equal(tencentCloudBodySuccess.result.success, true)
assert.equal(tencentCloudBodySuccess.result.method, 'rich-text')
assert.equal(tencentCloudBodySuccess.title, '腾讯云正文验证')
const tencentCloudBodyTruncated = await runSerializedTitleFill(
fillTencentCloudContent,
[
'腾讯云正文验证',
'# 腾讯云正文验证\n\n- 中间列表项目\n\n这是必须保留的结尾内容。',
'<h1>腾讯云正文验证</h1><ul><li>中间列表项目</li></ul><p>这是必须保留的结尾内容。</p>',
'腾讯云正文验证中间列表项目这是必须保留的结尾内容。',
],
{ bodyContentEditable: true, truncateBody: true },
)
assert.equal(tencentCloudBodyTruncated.result.success, false)
assert.equal(tencentCloudBodyTruncated.result.error, '腾讯云开发者社区未确认接收富文本正文')
const tencentCloudMarkdownWhitespaceLost = await runSerializedTitleFill(
fillTencentCloudContent,
[
'腾讯云 Markdown 正文验证',
'# 腾讯云 Markdown 正文验证\n\n- 列表项\n\n```js\nconst value = 1\n```',
'',
'',
],
{ bodyTextarea: true, stripBodyWhitespace: true },
)
assert.equal(tencentCloudMarkdownWhitespaceLost.result.success, false)
assert.equal(tencentCloudMarkdownWhitespaceLost.result.error, '腾讯云开发者社区未确认接收正文')
await assertSerializedTitleFill({
fillFunction: fillAliyunContent,
args: ['阿里云完整标题验证', '', '', ''],
error: '阿里云开发者社区未确认接收标题',
})
const aliyunBodySuccess = await runSerializedTitleFill(
fillAliyunContent,
[
'阿里云正文验证',
'# 阿里云正文验证\n\n- 中间列表项目\n\n这是必须保留的结尾内容。',
'<h1>阿里云正文验证</h1><ul><li>中间列表项目</li></ul><p>这是必须保留的结尾内容。</p>',
'阿里云正文验证中间列表项目这是必须保留的结尾内容。',
],
{ bodyTextarea: true },
)
assert.equal(aliyunBodySuccess.result.success, true)
assert.equal(aliyunBodySuccess.result.method, 'textarea')
assert.equal(aliyunBodySuccess.title, '阿里云正文验证')
const aliyunBodyTruncated = await runSerializedTitleFill(
fillAliyunContent,
[
'阿里云正文验证',
'# 阿里云正文验证\n\n- 中间列表项目\n\n这是必须保留的结尾内容。',
'<h1>阿里云正文验证</h1><ul><li>中间列表项目</li></ul><p>这是必须保留的结尾内容。</p>',
'阿里云正文验证中间列表项目这是必须保留的结尾内容。',
],
{ bodyTextarea: true, truncateBody: true },
)
assert.equal(aliyunBodyTruncated.result.success, false)
assert.equal(aliyunBodyTruncated.result.error, '阿里云开发者社区未确认接收正文,请在编辑器中手动粘贴后再重试')
await assertSerializedTitleFill({
fillFunction: fillCto51Content,
args: ['51CTO完整标题验证', '', '', ''],
error: '51CTO 未确认接收标题',
})
const cto51BodySuccess = await runSerializedTitleFill(
fillCto51Content,
[
'51CTO 正文验证',
'# 51CTO 正文验证\n\n这是用于核对 Markdown 全文写入的第一段内容。\n\n- 中间列表项目\n\n这是必须保留的结尾内容。',
'<h1>51CTO 正文验证</h1><p>这是用于核对 Markdown 全文写入的第一段内容。</p><ul><li>中间列表项目</li></ul><p>这是必须保留的结尾内容。</p>',
'51CTO 正文验证这是用于核对 Markdown 全文写入的第一段内容。中间列表项目这是必须保留的结尾内容。',
],
{ bodyTextarea: true },
)
assert.equal(cto51BodySuccess.result.success, true)
assert.equal(cto51BodySuccess.result.method, 'markdown-textarea')
assert.equal(cto51BodySuccess.title, '51CTO 正文验证')
const cto51BodyTruncated = await runSerializedTitleFill(
fillCto51Content,
[
'51CTO 正文验证',
'# 51CTO 正文验证\n\n这是用于核对 Markdown 全文写入的第一段内容。\n\n- 中间列表项目\n\n这是必须保留的结尾内容。',
'<h1>51CTO 正文验证</h1><p>这是用于核对 Markdown 全文写入的第一段内容。</p><ul><li>中间列表项目</li></ul><p>这是必须保留的结尾内容。</p>',
'51CTO 正文验证这是用于核对 Markdown 全文写入的第一段内容。中间列表项目这是必须保留的结尾内容。',
],
{ bodyTextarea: true, truncateBody: true },
)
assert.equal(cto51BodyTruncated.result.success, false)
assert.equal(cto51BodyTruncated.result.error, '51CTO 未确认接收正文,请在编辑器中手动粘贴后再重试')
await assertSerializedTitleFill({
fillFunction: fillOSChinaContent,
args: ['开源中国完整标题验证', '', '', ''],
error: '开源中国未确认接收标题',
})
const oschinaBodySuccess = await runSerializedTitleFill(
fillOSChinaContent,
[
'开源中国正文验证',
'# 开源中国正文验证\n\n这是用于核对 Markdown 全文写入的第一段内容。\n\n- 中间列表项目\n\n这是必须保留的结尾内容。',
'<h1>开源中国正文验证</h1><p>这是用于核对 Markdown 全文写入的第一段内容。</p><ul><li>中间列表项目</li></ul><p>这是必须保留的结尾内容。</p>',
'开源中国正文验证这是用于核对 Markdown 全文写入的第一段内容。中间列表项目这是必须保留的结尾内容。',
],
{ bodyTextarea: true },
)
assert.equal(oschinaBodySuccess.result.success, true)
assert.equal(oschinaBodySuccess.result.method, 'markdown-textarea')
assert.equal(oschinaBodySuccess.title, '开源中国正文验证')
const oschinaBodyTruncated = await runSerializedTitleFill(
fillOSChinaContent,
[
'开源中国正文验证',
'# 开源中国正文验证\n\n这是用于核对 Markdown 全文写入的第一段内容。\n\n- 中间列表项目\n\n这是必须保留的结尾内容。',
'<h1>开源中国正文验证</h1><p>这是用于核对 Markdown 全文写入的第一段内容。</p><ul><li>中间列表项目</li></ul><p>这是必须保留的结尾内容。</p>',
'开源中国正文验证这是用于核对 Markdown 全文写入的第一段内容。中间列表项目这是必须保留的结尾内容。',
],
{ bodyTextarea: true, truncateBody: true },
)
assert.equal(oschinaBodyTruncated.result.success, false)
assert.equal(oschinaBodyTruncated.result.error, '开源中国未确认接收正文,请在编辑器中手动粘贴后再重试')
await assertSerializedTitleFill({
fillFunction: fillToutiaoContent,
args: ['今日头条完整标题验证', '<p>今日头条隔离环境正文写入验证内容</p>', '今日头条隔离环境正文写入验证内容'],
bodyContentEditable: true,
error: '今日头条未确认接收标题',
expectedBodyText: '今日头条隔离环境正文写入验证内容',
expectedMethod: 'paste-html',
})
const toutiaoBodyTruncated = await runSerializedTitleFill(
fillToutiaoContent,
[
'今日头条正文验证',
'<h1>今日头条正文验证</h1><p>这是用于核对富文本转换完整性的第一段内容。</p><ul><li>中间列表项目</li></ul><p>这是必须保留的结尾内容。</p>',
'今日头条正文验证这是用于核对富文本转换完整性的第一段内容。中间列表项目这是必须保留的结尾内容。',
],
{ bodyContentEditable: true, truncateBody: true },
)
assert.equal(toutiaoBodyTruncated.result.success, false)
assert.equal(toutiaoBodyTruncated.result.error, '今日头条未确认接收正文,请在编辑器中手动粘贴后再重试')
const backgroundSource = await readFile(
new URL('../distribution/cose/background.js', import.meta.url),
'utf8',
@@ -1005,6 +1660,29 @@ assert.deepEqual(
},
)
await assertSerializedTitleFill({
fillFunction: fillCsdnContent,
args: ['CSDN 完整标题验证', '', ''],
error: 'CSDN 未确认接收标题',
})
const csdnBodySuccess = await runSerializedTitleFill(
fillCsdnContent,
['CSDN 正文验证', '# CSDN 正文验证\n\n- 中间列表项目\n\n这是必须保留的结尾内容。', ''],
{ bodyContentEditable: true },
)
assert.equal(csdnBodySuccess.result.success, true)
assert.equal(csdnBodySuccess.result.method, 'contenteditable')
assert.equal(csdnBodySuccess.title, 'CSDN 正文验证')
const csdnBodyTruncated = await runSerializedTitleFill(
fillCsdnContent,
['CSDN 正文验证', '# CSDN 正文验证\n\n- 中间列表项目\n\n这是必须保留的结尾内容。', ''],
{ bodyContentEditable: true, truncateBody: true },
)
assert.equal(csdnBodyTruncated.result.success, false)
assert.equal(csdnBodyTruncated.result.error, 'CSDN 未确认接收正文,请在编辑器中手动粘贴后再重试')
const segmentFaultSession = JSON.stringify({
props: {
pageProps: {
@@ -1068,6 +1746,36 @@ assert.deepEqual(
message: '已确认思否文章公开地址',
},
)
await assertSerializedTitleFill({
fillFunction: fillSegmentFaultContent,
args: ['思否完整标题验证', '', ''],
error: '思否未确认接收标题',
})
const segmentFaultBodySuccess = await runSerializedTitleFill(
fillSegmentFaultContent,
[
'思否正文验证',
'# 思否正文验证\n\n这是用于核对 Markdown 全文写入的第一段内容。\n\n- 中间列表项目\n\n这是必须保留的结尾内容。',
'',
],
{ bodyContentEditable: true },
)
assert.equal(segmentFaultBodySuccess.result.success, true)
assert.equal(segmentFaultBodySuccess.result.method, 'contenteditable')
assert.equal(segmentFaultBodySuccess.title, '思否正文验证')
const segmentFaultBodyTruncated = await runSerializedTitleFill(
fillSegmentFaultContent,
[
'思否正文验证',
'# 思否正文验证\n\n这是用于核对 Markdown 全文写入的第一段内容。\n\n- 中间列表项目\n\n这是必须保留的结尾内容。',
'',
],
{ bodyContentEditable: true, truncateBody: true },
)
assert.equal(segmentFaultBodyTruncated.result.success, false)
assert.equal(segmentFaultBodyTruncated.result.error, '思否未确认接收正文,请在编辑器中手动粘贴后再重试')
assert.deepEqual(
parseInfoQAccount({
@@ -1132,6 +1840,40 @@ assert.equal(
assert.equal(typeof SYNC_HANDLERS.infoq, 'function')
assert.equal(typeof INSPECT_HANDLERS.infoq, 'function')
await assertSerializedTitleFill({
fillFunction: fillInfoQContent,
args: ['InfoQ 完整标题验证', '', '', ''],
bodyContentEditable: true,
error: 'InfoQ 未确认接收标题',
})
const infoqBodySuccess = await runSerializedTitleFill(
fillInfoQContent,
[
'InfoQ 正文验证',
'# InfoQ 正文验证\n\n这是用于核对富文本转换完整性的第一段内容。\n\n- 中间列表项目\n\n这是必须保留的结尾内容。',
'<h1>InfoQ 正文验证</h1><p>这是用于核对富文本转换完整性的第一段内容。</p><ul><li>中间列表项目</li></ul><p>这是必须保留的结尾内容。</p>',
'InfoQ 正文验证这是用于核对富文本转换完整性的第一段内容。中间列表项目这是必须保留的结尾内容。',
],
{ bodyContentEditable: true },
)
assert.equal(infoqBodySuccess.result.success, true)
assert.equal(infoqBodySuccess.result.method, 'paste-html')
assert.equal(infoqBodySuccess.title, 'InfoQ 正文验证')
const infoqBodyTruncated = await runSerializedTitleFill(
fillInfoQContent,
[
'InfoQ 正文验证',
'# InfoQ 正文验证\n\n这是用于核对富文本转换完整性的第一段内容。\n\n- 中间列表项目\n\n这是必须保留的结尾内容。',
'<h1>InfoQ 正文验证</h1><p>这是用于核对富文本转换完整性的第一段内容。</p><ul><li>中间列表项目</li></ul><p>这是必须保留的结尾内容。</p>',
'InfoQ 正文验证这是用于核对富文本转换完整性的第一段内容。中间列表项目这是必须保留的结尾内容。',
],
{ bodyContentEditable: true, truncateBody: true },
)
assert.equal(infoqBodyTruncated.result.success, false)
assert.equal(infoqBodyTruncated.result.error, 'InfoQ 未确认接收正文,请在编辑器中手动粘贴后再重试')
const infoqDetectorSource = await readFile(
new URL('../distribution/cose/detection/src/platforms/infoq.js', import.meta.url),
'utf8',