Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6f55d2c809 |
@@ -12,9 +12,9 @@ AiToEarn 自维护的 Chrome/Edge 扩展,也是 Gitea Release 唯一允许打
|
||||
|
||||
## Browser Provider
|
||||
|
||||
AiToEarn 的 Browser Provider 使用当前 Chrome 登录态,不把平台 Cookie、Token 或 CSRF 信息上传到服务端。当前已接入掘金、简书和知乎:扩展读取平台稳定 UID,核对它与 AiToEarn 绑定账号一致后,在持久化发布任务中打开并填充 Markdown 编辑器。
|
||||
AiToEarn 的 Browser Provider 使用当前 Chrome 登录态,不把平台 Cookie、Token 或 CSRF 信息上传到服务端。当前已接入掘金、简书、知乎和今日头条:扩展读取平台稳定 UID,核对它与 AiToEarn 绑定账号一致后,在持久化发布任务中打开并填充编辑器。今日头条会将 Markdown 转为安全的富文本粘贴,保留标题、段落、列表、链接和图片。
|
||||
|
||||
任务会复用已有编辑器标签页。标签页仍位于已保存草稿或公开文章时,扩展只返回并激活该标签页;如果标签页已离开任务页面,则在同一标签页重新进入编辑器并填充,不重复创建窗口。掘金只有 `/post/:id`、简书只有 `/p/:id`、知乎只有 `zhuanlan.zhihu.com/p/:id` 才记为已发布;知乎 `/write/:id` 记为草稿,无法确定时进入“发布结果待确认”,不会伪报成功。
|
||||
任务会复用已有编辑器标签页。标签页仍位于已保存草稿或公开文章时,扩展只返回并激活该标签页;如果标签页已离开任务页面,则在同一标签页重新进入编辑器并填充,不重复创建窗口。掘金只有 `/post/:id`、简书只有 `/p/:id`、知乎只有 `zhuanlan.zhihu.com/p/:id`、今日头条只有 `www.toutiao.com/article/:id` 或 `www.toutiao.com/item/:id` 才记为已发布;知乎 `/write/:id` 和今日头条带 `draft_id` 的编辑器地址记为草稿,无法确定时进入“发布结果待确认”,不会伪报成功。
|
||||
|
||||
扩展默认只打开并填充编辑器,不替用户点击公开发布。微信公众号保存草稿及任何平台的最终发布仍需用户明确确认。
|
||||
|
||||
|
||||
@@ -11,7 +11,11 @@ import {
|
||||
ZhihuPlatform,
|
||||
syncZhihuContent,
|
||||
} from './zhihu.js'
|
||||
import { ToutiaoPlatform, syncToutiaoContent } from './toutiao.js'
|
||||
import {
|
||||
inspectToutiaoTask,
|
||||
ToutiaoPlatform,
|
||||
syncToutiaoContent,
|
||||
} from './toutiao.js'
|
||||
import { SegmentFaultPlatform } from './segmentfault.js'
|
||||
import { CnblogsPlatform } from './cnblogs.js'
|
||||
import { OSChinaPlatform } from './oschina.js'
|
||||
@@ -131,6 +135,7 @@ const INSPECT_HANDLERS = {
|
||||
juejin: tab => inspectJuejinTaskState(tab?.url || ''),
|
||||
jianshu: inspectJianshuTask,
|
||||
zhihu: inspectZhihuTask,
|
||||
toutiao: inspectToutiaoTask,
|
||||
}
|
||||
|
||||
// 导出
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// 今日头条平台配置
|
||||
const ToutiaoPlatform = {
|
||||
id: 'toutiao',
|
||||
name: 'Toutiao',
|
||||
@@ -9,134 +8,434 @@ const ToutiaoPlatform = {
|
||||
type: 'toutiao',
|
||||
}
|
||||
|
||||
import { injectUtils } from './common.js'
|
||||
function parseToutiaoPublishedUrl(rawUrl) {
|
||||
if (!rawUrl) return null
|
||||
|
||||
// 今日头条内容填充函数(在页面主世界中执行)
|
||||
function fillToutiaoContentInPage(title, body) {
|
||||
// 等待满足条件的元素出现
|
||||
function waitForElement(predicate, timeout = 10000) {
|
||||
try {
|
||||
const url = new URL(rawUrl, 'https://mp.toutiao.com')
|
||||
if (url.protocol !== 'https:' || !['toutiao.com', 'www.toutiao.com'].includes(url.hostname)) {
|
||||
return null
|
||||
}
|
||||
|
||||
const published = url.pathname.match(/^\/(?:article|item|w)\/([^/?#]+)\/?$/)
|
||||
if (!published?.[1]) return null
|
||||
|
||||
return {
|
||||
status: 'published',
|
||||
url: url.href,
|
||||
platformWorkId: published[1],
|
||||
message: '已确认今日头条文章公开地址',
|
||||
}
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function getToutiaoDraftId(url) {
|
||||
for (const key of ['draft_id', 'draftId']) {
|
||||
const value = url.searchParams.get(key)
|
||||
if (value?.trim()) return value.trim()
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
function inspectToutiaoTaskState(rawUrl, publicUrl) {
|
||||
const published = parseToutiaoPublishedUrl(publicUrl) || parseToutiaoPublishedUrl(rawUrl)
|
||||
if (published) return published
|
||||
|
||||
let url
|
||||
try {
|
||||
url = new URL(rawUrl)
|
||||
} catch {
|
||||
return { status: 'failed', message: '无法读取今日头条标签页地址' }
|
||||
}
|
||||
|
||||
if (url.protocol !== 'https:' || url.hostname !== 'mp.toutiao.com') {
|
||||
return {
|
||||
status: 'publication_uncertain',
|
||||
message: '当前标签页已经离开今日头条,无法确认发布结果',
|
||||
}
|
||||
}
|
||||
|
||||
if (/^\/profile_v4\/graphic\/publish\/?$/.test(url.pathname)) {
|
||||
const draftId = getToutiaoDraftId(url)
|
||||
if (draftId) {
|
||||
return {
|
||||
status: 'draft_saved',
|
||||
url: url.href,
|
||||
platformWorkId: draftId,
|
||||
message: '已确认今日头条草稿地址',
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
status: 'filled',
|
||||
url: url.href,
|
||||
message: '内容已填充到今日头条编辑器,请检查后保存草稿或发布',
|
||||
}
|
||||
}
|
||||
|
||||
if (/^\/profile_v4\/graphic\/articles\/?$/.test(url.pathname)) {
|
||||
return {
|
||||
status: 'publication_uncertain',
|
||||
url: url.href,
|
||||
message: '已回到今日头条文章列表,请打开本次文章核对发布结果',
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
status: 'publication_uncertain',
|
||||
url: url.href,
|
||||
message: '当前今日头条页面无法确认是草稿还是已发布文章',
|
||||
}
|
||||
}
|
||||
|
||||
function escapeHtml(value) {
|
||||
return String(value)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''')
|
||||
}
|
||||
|
||||
function safeUrl(value) {
|
||||
try {
|
||||
const url = new URL(value)
|
||||
return ['http:', 'https:'].includes(url.protocol) ? url.href : null
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function renderToutiaoInline(value) {
|
||||
const pattern = /!\[([^\]]*)\]\((https?:\/\/[^)\s]+)(?:\s+"[^"]*")?\)|\[([^\]]+)\]\((https?:\/\/[^)\s]+)(?:\s+"[^"]*")?\)|`([^`]+)`|\*\*([^*]+)\*\*|__([^_]+)__|\*([^*]+)\*|_([^_]+)_/g
|
||||
let html = ''
|
||||
let cursor = 0
|
||||
|
||||
for (const match of value.matchAll(pattern)) {
|
||||
html += escapeHtml(value.slice(cursor, match.index))
|
||||
if (match[1]) {
|
||||
const url = safeUrl(match[2])
|
||||
html += url
|
||||
? `<img src="${escapeHtml(url)}" alt="${escapeHtml(match[1])}" />`
|
||||
: escapeHtml(match[0])
|
||||
} else if (match[3]) {
|
||||
const url = safeUrl(match[4])
|
||||
html += url
|
||||
? `<a href="${escapeHtml(url)}">${escapeHtml(match[3])}</a>`
|
||||
: escapeHtml(match[0])
|
||||
} else if (match[5]) {
|
||||
html += `<code>${escapeHtml(match[5])}</code>`
|
||||
} else if (match[6] || match[7]) {
|
||||
html += `<strong>${escapeHtml(match[6] || match[7])}</strong>`
|
||||
} else {
|
||||
html += `<em>${escapeHtml(match[8] || match[9])}</em>`
|
||||
}
|
||||
cursor = match.index + match[0].length
|
||||
}
|
||||
|
||||
return html + escapeHtml(value.slice(cursor))
|
||||
}
|
||||
|
||||
function renderToutiaoMarkdown(markdown) {
|
||||
const lines = String(markdown || '').replace(/\r\n?/g, '\n').split('\n')
|
||||
const blocks = []
|
||||
let paragraph = []
|
||||
let listType
|
||||
let listItems = []
|
||||
let codeLines
|
||||
|
||||
const flushParagraph = () => {
|
||||
if (paragraph.length) {
|
||||
blocks.push(`<p>${paragraph.map(renderToutiaoInline).join('<br />')}</p>`)
|
||||
paragraph = []
|
||||
}
|
||||
}
|
||||
|
||||
const flushList = () => {
|
||||
if (!listType) return
|
||||
blocks.push(`<${listType}>${listItems.map(item => `<li>${renderToutiaoInline(item)}</li>`).join('')}</${listType}>`)
|
||||
listType = undefined
|
||||
listItems = []
|
||||
}
|
||||
|
||||
const flushBlocks = () => {
|
||||
flushParagraph()
|
||||
flushList()
|
||||
}
|
||||
|
||||
for (const line of lines) {
|
||||
if (codeLines) {
|
||||
if (/^\s*```/.test(line)) {
|
||||
blocks.push(`<pre><code>${escapeHtml(codeLines.join('\n'))}</code></pre>`)
|
||||
codeLines = undefined
|
||||
} else {
|
||||
codeLines.push(line)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if (/^\s*```/.test(line)) {
|
||||
flushBlocks()
|
||||
codeLines = []
|
||||
continue
|
||||
}
|
||||
|
||||
if (!line.trim()) {
|
||||
flushBlocks()
|
||||
continue
|
||||
}
|
||||
|
||||
const heading = line.match(/^\s{0,3}(#{1,6})\s+(.+?)\s*#*\s*$/)
|
||||
if (heading) {
|
||||
flushBlocks()
|
||||
const level = heading[1].length
|
||||
blocks.push(`<h${level}>${renderToutiaoInline(heading[2])}</h${level}>`)
|
||||
continue
|
||||
}
|
||||
|
||||
const unordered = line.match(/^\s*[-*+]\s+(.+)$/)
|
||||
const ordered = line.match(/^\s*\d+[.)]\s+(.+)$/)
|
||||
if (unordered || ordered) {
|
||||
flushParagraph()
|
||||
const nextType = unordered ? 'ul' : 'ol'
|
||||
if (listType && listType !== nextType) flushList()
|
||||
listType = nextType
|
||||
listItems.push((unordered || ordered)[1])
|
||||
continue
|
||||
}
|
||||
|
||||
const quote = line.match(/^\s*>\s?(.*)$/)
|
||||
if (quote) {
|
||||
flushBlocks()
|
||||
blocks.push(`<blockquote>${renderToutiaoInline(quote[1])}</blockquote>`)
|
||||
continue
|
||||
}
|
||||
|
||||
if (/^\s{0,3}([-*_])(?:\s*\1){2,}\s*$/.test(line)) {
|
||||
flushBlocks()
|
||||
blocks.push('<hr />')
|
||||
continue
|
||||
}
|
||||
|
||||
if (listType) flushList()
|
||||
paragraph.push(line)
|
||||
}
|
||||
|
||||
if (codeLines) blocks.push(`<pre><code>${escapeHtml(codeLines.join('\n'))}</code></pre>`)
|
||||
flushBlocks()
|
||||
return blocks.join('')
|
||||
}
|
||||
|
||||
function renderToutiaoPlainText(markdown) {
|
||||
return String(markdown || '')
|
||||
.replace(/!\[([^\]]*)\]\([^)]*\)/g, '$1')
|
||||
.replace(/\[([^\]]+)\]\([^)]*\)/g, '$1')
|
||||
.replace(/[`*_#>~-]/g, '')
|
||||
.replace(/\s+/g, '')
|
||||
}
|
||||
|
||||
function fillToutiaoContent(title, markdown, body) {
|
||||
const contentToFill = markdown || body || ''
|
||||
const sleep = milliseconds => new Promise(resolve => setTimeout(resolve, milliseconds))
|
||||
const normalizeText = value => String(value || '').replace(/\s+/g, '')
|
||||
|
||||
function findFirst(selectors) {
|
||||
return selectors.map(selector => document.querySelector(selector)).find(Boolean)
|
||||
}
|
||||
|
||||
function waitForElement(selectors, timeout = 15000) {
|
||||
return new Promise(resolve => {
|
||||
const el = predicate()
|
||||
if (el) return resolve(el)
|
||||
const existing = findFirst(selectors)
|
||||
if (existing) {
|
||||
resolve(existing)
|
||||
return
|
||||
}
|
||||
|
||||
const observer = new MutationObserver(() => {
|
||||
const el = predicate()
|
||||
if (el) {
|
||||
observer.disconnect()
|
||||
resolve(el)
|
||||
}
|
||||
})
|
||||
observer.observe(document.body, { childList: true, subtree: true })
|
||||
|
||||
setTimeout(() => {
|
||||
const element = findFirst(selectors)
|
||||
if (!element) return
|
||||
observer.disconnect()
|
||||
resolve(predicate())
|
||||
resolve(element)
|
||||
})
|
||||
observer.observe(document.documentElement, { childList: true, subtree: true })
|
||||
window.setTimeout(() => {
|
||||
observer.disconnect()
|
||||
resolve(findFirst(selectors))
|
||||
}, timeout)
|
||||
})
|
||||
}
|
||||
|
||||
async function fillContent() {
|
||||
// 填充标题 - 头条使用 textarea
|
||||
const titleInput = await waitForElement(() =>
|
||||
document.querySelector('textarea[placeholder*="标题"]')
|
||||
)
|
||||
if (titleInput && title) {
|
||||
titleInput.focus()
|
||||
// 模拟用户输入
|
||||
const nativeSetter = Object.getOwnPropertyDescriptor(
|
||||
window.HTMLTextAreaElement.prototype,
|
||||
'value'
|
||||
).set
|
||||
nativeSetter.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 }))
|
||||
console.log('[COSE] 头条标题填充成功:', title)
|
||||
} else {
|
||||
console.log('[COSE] 头条未找到标题输入框')
|
||||
function setInputValue(input, value) {
|
||||
const prototype = input instanceof HTMLTextAreaElement
|
||||
? window.HTMLTextAreaElement.prototype
|
||||
: window.HTMLInputElement.prototype
|
||||
const setter = Object.getOwnPropertyDescriptor(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 }))
|
||||
}
|
||||
|
||||
function selectEditorContents(editor) {
|
||||
const selection = window.getSelection()
|
||||
const range = document.createRange()
|
||||
range.selectNodeContents(editor)
|
||||
selection?.removeAllRanges()
|
||||
selection?.addRange(range)
|
||||
}
|
||||
|
||||
function dispatchHtmlPaste(editor, html, plainText) {
|
||||
editor.focus()
|
||||
selectEditorContents(editor)
|
||||
if (typeof DataTransfer !== 'undefined' && typeof ClipboardEvent !== 'undefined') {
|
||||
const clipboardData = new DataTransfer()
|
||||
clipboardData.setData('text/html', html)
|
||||
clipboardData.setData('text/plain', plainText)
|
||||
editor.dispatchEvent(new ClipboardEvent('paste', {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
clipboardData,
|
||||
}))
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
async function fill() {
|
||||
const titleInput = await waitForElement([
|
||||
'.editor-title textarea',
|
||||
'textarea[placeholder*="标题"]',
|
||||
'input[placeholder*="标题"]',
|
||||
])
|
||||
if (!titleInput) return { success: false, error: '未找到今日头条标题输入框' }
|
||||
if (title) setInputValue(titleInput, title)
|
||||
|
||||
const editor = await waitForElement([
|
||||
'.ProseMirror[contenteditable="true"]',
|
||||
'.ProseMirror',
|
||||
'[contenteditable="true"][role="textbox"]',
|
||||
])
|
||||
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 }))
|
||||
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
|
||||
let imageCount = editor.querySelectorAll('img').length
|
||||
|
||||
if (editorTextLength + imageCount * 12 < minimumLength && typeof document.execCommand === 'function') {
|
||||
editor.focus()
|
||||
selectEditorContents(editor)
|
||||
document.execCommand('insertHTML', false, html)
|
||||
editor.dispatchEvent(new InputEvent('input', { bubbles: true, inputType: 'insertFromPaste', data: plainText }))
|
||||
await sleep(500)
|
||||
editorTextLength = normalizeText(editor.innerText || editor.textContent).length
|
||||
imageCount = editor.querySelectorAll('img').length
|
||||
}
|
||||
|
||||
// 等待编辑器加载
|
||||
await new Promise(resolve => setTimeout(resolve, 500))
|
||||
if (editorTextLength + imageCount * 12 < minimumLength) {
|
||||
return {
|
||||
success: false,
|
||||
error: '今日头条未确认接收正文,请在编辑器中手动粘贴后再重试',
|
||||
}
|
||||
}
|
||||
|
||||
// 头条使用 ProseMirror 富文本编辑器
|
||||
const editor = await waitForElement(() => document.querySelector('.ProseMirror'))
|
||||
|
||||
if (editor && body) {
|
||||
editor.focus()
|
||||
|
||||
// 对于 ProseMirror,我们需要更智能的方式来填充内容
|
||||
// 清空现有内容
|
||||
editor.innerHTML = ''
|
||||
|
||||
// 将内容分割成段落
|
||||
const lines = body.split('\n').filter(line => line.trim() !== '')
|
||||
|
||||
// 使用 document.execCommand 插入内容(ProseMirror 兼容)
|
||||
const selection = window.getSelection()
|
||||
const range = document.createRange()
|
||||
range.selectNodeContents(editor)
|
||||
range.collapse(false)
|
||||
selection.removeAllRanges()
|
||||
selection.addRange(range)
|
||||
|
||||
// 创建 HTML 内容
|
||||
const htmlContent = lines.map(line => `<p>${line}</p>`).join('')
|
||||
|
||||
// 使用 insertHTML 命令
|
||||
document.execCommand('insertHTML', false, htmlContent)
|
||||
|
||||
// 触发事件让 ProseMirror 同步
|
||||
editor.dispatchEvent(new InputEvent('input', { bubbles: true }))
|
||||
editor.dispatchEvent(new Event('change', { bubbles: true }))
|
||||
|
||||
console.log('[COSE] 头条内容填充成功')
|
||||
return { success: true }
|
||||
} else {
|
||||
console.log('[COSE] 头条未找到编辑器')
|
||||
return { success: false, error: '未找到编辑器' }
|
||||
return {
|
||||
success: true,
|
||||
method: 'paste-html',
|
||||
imageCount,
|
||||
}
|
||||
}
|
||||
|
||||
return fillContent()
|
||||
return fill()
|
||||
}
|
||||
|
||||
async function inspectToutiaoTask(tab, helpers) {
|
||||
const currentState = inspectToutiaoTaskState(tab?.url || '')
|
||||
if (!tab?.id || currentState.status === 'published') return currentState
|
||||
|
||||
try {
|
||||
const result = await helpers.chrome.scripting.executeScript({
|
||||
target: { tabId: tab.id },
|
||||
func: () => {
|
||||
const publicLink = Array.from(document.querySelectorAll('a[href]')).find(link => {
|
||||
try {
|
||||
const url = new URL(link.getAttribute('href'), location.href)
|
||||
return ['toutiao.com', 'www.toutiao.com'].includes(url.hostname)
|
||||
&& /^\/(?:article|item|w)\/[^/]+\/?$/.test(url.pathname)
|
||||
&& (link.target === '_blank' || /查看文章|阅读文章|发布成功|已发布/.test(link.textContent || ''))
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
})
|
||||
const visibleText = document.body?.innerText || ''
|
||||
return {
|
||||
publicUrl: publicLink
|
||||
? new URL(publicLink.getAttribute('href'), location.href).href
|
||||
: undefined,
|
||||
draftSaved: /草稿已保存|已保存草稿|保存成功/.test(visibleText),
|
||||
}
|
||||
},
|
||||
})
|
||||
const observed = result?.[0]?.result
|
||||
const state = inspectToutiaoTaskState(tab.url || '', observed?.publicUrl)
|
||||
if (state.status === 'filled' && observed?.draftSaved) {
|
||||
return {
|
||||
...state,
|
||||
status: 'draft_saved',
|
||||
message: '已确认今日头条编辑器显示草稿保存成功',
|
||||
}
|
||||
}
|
||||
return state
|
||||
} catch {
|
||||
return currentState
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 今日头条同步处理器
|
||||
* @param {object} tab - Chrome tab 对象
|
||||
* @param {object} content - 内容对象 { title, body, markdown }
|
||||
* @param {object} helpers - 帮助函数 { chrome, waitForTab, addTabToSyncGroup }
|
||||
* @returns {Promise<{success: boolean, message?: string, tabId?: number}>}
|
||||
*/
|
||||
async function syncToutiaoContent(tab, content, helpers) {
|
||||
const { chrome, waitForTab } = helpers
|
||||
if (!tab?.id) return { success: false, message: '无法获取今日头条编辑器标签页' }
|
||||
|
||||
// 等待页面加载完成
|
||||
await waitForTab(tab.id)
|
||||
|
||||
// 额外等待一下让编辑器完全加载
|
||||
await new Promise(resolve => setTimeout(resolve, 2500))
|
||||
|
||||
// 先注入公共工具函数
|
||||
await injectUtils(chrome, tab.id)
|
||||
|
||||
// 在页面中执行填充
|
||||
const result = await chrome.scripting.executeScript({
|
||||
await helpers.waitForTab(tab.id)
|
||||
const result = await helpers.chrome.scripting.executeScript({
|
||||
target: { tabId: tab.id },
|
||||
func: fillToutiaoContentInPage,
|
||||
args: [content.title, content.body || content.markdown || ''],
|
||||
func: fillToutiaoContent,
|
||||
args: [content.title, content.markdown, content.body],
|
||||
world: 'MAIN',
|
||||
})
|
||||
|
||||
const fillResult = result?.[0]?.result
|
||||
if (fillResult?.success) {
|
||||
return { success: true, message: '已打开头条号并填充内容', tabId: tab.id }
|
||||
} else {
|
||||
return { success: false, message: fillResult?.error || '内容填充失败', tabId: tab.id }
|
||||
if (!fillResult?.success) {
|
||||
return {
|
||||
success: false,
|
||||
message: fillResult?.error || '今日头条内容填充失败',
|
||||
tabId: tab.id,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: '已打开并填充今日头条编辑器,请检查后保存草稿或发布',
|
||||
tabId: tab.id,
|
||||
}
|
||||
}
|
||||
|
||||
// 导出
|
||||
export { ToutiaoPlatform, fillToutiaoContentInPage, syncToutiaoContent }
|
||||
export {
|
||||
ToutiaoPlatform,
|
||||
fillToutiaoContent,
|
||||
inspectToutiaoTask,
|
||||
inspectToutiaoTaskState,
|
||||
parseToutiaoPublishedUrl,
|
||||
renderToutiaoMarkdown,
|
||||
renderToutiaoPlainText,
|
||||
syncToutiaoContent,
|
||||
}
|
||||
|
||||
@@ -34,14 +34,32 @@ export const ZhihuLoginConfig = {
|
||||
}
|
||||
|
||||
// 头条号
|
||||
export function parseToutiaoAccount(response) {
|
||||
const data = response?.data && typeof response.data === 'object' ? response.data : response
|
||||
const user = data?.user && typeof data.user === 'object' ? data.user : undefined
|
||||
const platformUid = [
|
||||
data?.user_id,
|
||||
data?.uid,
|
||||
data?.creator_id,
|
||||
data?.account_id,
|
||||
data?.id,
|
||||
user?.user_id,
|
||||
user?.uid,
|
||||
user?.id,
|
||||
].find(value => typeof value === 'string' || typeof value === 'number')
|
||||
|
||||
return {
|
||||
platformUid: platformUid === undefined || platformUid === null ? undefined : String(platformUid),
|
||||
username: data?.name || data?.nickname || user?.name || user?.nickname,
|
||||
avatar: data?.avatar_url || data?.avatar || user?.avatar_url || user?.avatar,
|
||||
}
|
||||
}
|
||||
|
||||
export const ToutiaoLoginConfig = {
|
||||
api: 'https://mp.toutiao.com/mp/agw/creator_center/user_info?app_id=1231',
|
||||
method: 'GET',
|
||||
checkLogin: response => response?.code === 0 && response?.name,
|
||||
getUserInfo: response => ({
|
||||
username: response?.name,
|
||||
avatar: response?.avatar_url,
|
||||
}),
|
||||
checkLogin: response => response?.code === 0 && Boolean(parseToutiaoAccount(response).platformUid),
|
||||
getUserInfo: parseToutiaoAccount,
|
||||
}
|
||||
|
||||
// 百家号
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"name": "AiToEarn - 内容营销助手",
|
||||
"version": "1.6.0",
|
||||
"version": "1.7.0",
|
||||
"description": "AiToEarn 自维护内容扩展:网页采集、账号检测、多平台文章分发与受控人工互动",
|
||||
"permissions": [
|
||||
"activeTab",
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { ZhihuLoginConfig } from '../distribution/cose/detection/src/configs.js'
|
||||
import {
|
||||
ToutiaoLoginConfig,
|
||||
ZhihuLoginConfig,
|
||||
} from '../distribution/cose/detection/src/configs.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 {
|
||||
inspectToutiaoTaskState,
|
||||
renderToutiaoMarkdown,
|
||||
} from '../distribution/cose/core/platforms/toutiao.js'
|
||||
import { canReturnExistingPlatformTaskState } from '../distribution/cose/core/task-state.js'
|
||||
|
||||
assert.deepEqual(
|
||||
@@ -155,4 +162,65 @@ assert.equal(
|
||||
true,
|
||||
)
|
||||
|
||||
assert.deepEqual(
|
||||
ToutiaoLoginConfig.getUserInfo({
|
||||
code: 0,
|
||||
data: {
|
||||
user_id: 'toutiao-user-1',
|
||||
name: '头条作者',
|
||||
avatar_url: 'https://p3-sign.toutiaoimg.com/avatar.jpg',
|
||||
},
|
||||
}),
|
||||
{
|
||||
platformUid: 'toutiao-user-1',
|
||||
username: '头条作者',
|
||||
avatar: 'https://p3-sign.toutiaoimg.com/avatar.jpg',
|
||||
},
|
||||
)
|
||||
assert.equal(
|
||||
ToutiaoLoginConfig.checkLogin({ code: 0, data: { name: '无稳定标识' } }),
|
||||
false,
|
||||
)
|
||||
|
||||
assert.deepEqual(
|
||||
inspectToutiaoTaskState('https://mp.toutiao.com/profile_v4/graphic/publish'),
|
||||
{
|
||||
status: 'filled',
|
||||
url: 'https://mp.toutiao.com/profile_v4/graphic/publish',
|
||||
message: '内容已填充到今日头条编辑器,请检查后保存草稿或发布',
|
||||
},
|
||||
)
|
||||
assert.deepEqual(
|
||||
inspectToutiaoTaskState('https://mp.toutiao.com/profile_v4/graphic/publish?draft_id=123456'),
|
||||
{
|
||||
status: 'draft_saved',
|
||||
url: 'https://mp.toutiao.com/profile_v4/graphic/publish?draft_id=123456',
|
||||
platformWorkId: '123456',
|
||||
message: '已确认今日头条草稿地址',
|
||||
},
|
||||
)
|
||||
assert.deepEqual(
|
||||
inspectToutiaoTaskState('https://www.toutiao.com/article/987654321/'),
|
||||
{
|
||||
status: 'published',
|
||||
url: 'https://www.toutiao.com/article/987654321/',
|
||||
platformWorkId: '987654321',
|
||||
message: '已确认今日头条文章公开地址',
|
||||
},
|
||||
)
|
||||
assert.equal(
|
||||
inspectToutiaoTaskState('https://mp.toutiao.com/profile_v4/graphic/articles').status,
|
||||
'publication_uncertain',
|
||||
)
|
||||
assert.equal(
|
||||
canReturnExistingPlatformTaskState(
|
||||
inspectToutiaoTaskState('https://mp.toutiao.com/profile_v4/graphic/publish?draftId=789'),
|
||||
),
|
||||
true,
|
||||
)
|
||||
assert.match(
|
||||
renderToutiaoMarkdown('# 标题\n\n- 第一项\n- 第二项\n\n\n\n**重点**'),
|
||||
/<h1>标题<\/h1><ul><li>第一项<\/li><li>第二项<\/li><\/ul><p><img src="https:\/\/example\.com\/image\.png" alt="配图" \/><\/p><p><strong>重点<\/strong><\/p>/,
|
||||
)
|
||||
|
||||
console.log('Distribution handoff tests passed')
|
||||
|
||||
Reference in New Issue
Block a user