Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ef64467f6b | |||
| c2d2a6c749 | |||
| 537df968a0 | |||
| dd9e628ac2 | |||
| 679e949703 | |||
| 984d646e1d | |||
| 52e8f7f4a0 | |||
| da8ad24f14 | |||
| 6561378bc1 | |||
| be650f6fab |
@@ -0,0 +1,3 @@
|
||||
* text=auto eol=lf
|
||||
*.png binary
|
||||
*.zip binary
|
||||
@@ -12,7 +12,9 @@ AiToEarn 自维护的 Chrome/Edge 扩展,也是 Gitea Release 唯一允许打
|
||||
|
||||
## Browser Provider
|
||||
|
||||
AiToEarn 的 Browser Provider 使用当前 Chrome 登录态,不把平台 Cookie、Token 或 CSRF 信息上传到服务端。当前已接入 CSDN、掘金、简书、知乎、今日头条、百家号、网易号、搜狐号、InfoQ、腾讯云开发者社区、阿里云开发者社区、思否、51CTO 和开源中国:扩展读取平台稳定 UID,核对它与 AiToEarn 绑定账号一致后,在持久化发布任务中打开并填充编辑器。今日头条、百家号、网易号、搜狐号和 InfoQ 会将 Markdown 转为安全的富文本,保留标题、段落、列表、链接和图片。CSDN 只使用编辑器自带的签名请求客户端读取创作者资料,思否只从当前页面的 Next.js 会话数据读取 UID;网易号只接受创作者侧返回的 `wemediaId`,搜狐号只接受账号列表中的账号 ID,InfoQ 只接受用户接口返回的 UID,51CTO 只接受个人空间页面中可验证的稳定 UID,开源中国只接受官方账号接口返回的稳定 UID。
|
||||
AiToEarn 的 Browser Provider 使用当前 Chrome 登录态,不把平台 Cookie、Token 或 CSRF 信息上传到服务端。当前已接入 CSDN、掘金、简书、知乎、今日头条、百家号、网易号、搜狐号、InfoQ、腾讯云开发者社区、阿里云开发者社区、思否、51CTO、开源中国和腾讯内容开放平台(企鹅号):扩展读取平台稳定 UID,核对它与 AiToEarn 绑定账号一致后,在持久化发布任务中打开并填充编辑器。今日头条、百家号、网易号、搜狐号和 InfoQ 会将 Markdown 转为安全的富文本,保留标题、段落、列表、链接和图片。CSDN 只使用编辑器自带的签名请求客户端读取创作者资料,思否只从当前页面的 Next.js 会话数据读取 UID;网易号只接受创作者侧返回的 `wemediaId`,搜狐号只接受账号列表中的账号 ID,InfoQ 只接受用户接口返回的 UID,51CTO 只接受个人空间页面中可验证的稳定 UID,开源中国只接受官方账号接口返回的稳定 UID,企鹅号只接受页面中的稳定 `userID` / `userId`。
|
||||
|
||||
扩展支持服务端随发布任务下发经过签名安装、Schema 校验并固化版本身份的声明式 Browser Provider 快照。插件只能覆盖核心十五个平台的标题、正文与第一层草稿/发布按钮定义;平台 URL、账号检测和最终结果判断仍由扩展核心维护。扩展会再次严格校验平台、选择器、编辑器类型、内容格式、源提交和内容哈希,写入后回读标题、正文长度和图片数量。配置漂移或回读失败时保留编辑器并返回明确失败,不执行仓库代码,也不会把未知按钮当作核心动作继续点击。
|
||||
|
||||
Browser Provider 仅负责账号核对、编辑器填充和草稿/公开文章状态回读,不会自动点击最终公开发布按钮。所有已接入平台仍保留复制发布稿入口;未进入受控交接白名单的平台继续只支持手动复制,不会伪报“已同步”。
|
||||
|
||||
@@ -56,7 +58,7 @@ AiToEarn 自行维护网页桥接、载荷协议、产品界面、打包和发
|
||||
powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\scripts\package.ps1
|
||||
```
|
||||
|
||||
脚本会校验 `manifest.json`、全部 JavaScript 语法和必需文件,再输出:
|
||||
脚本会校验 `manifest.json`、全部 JavaScript 语法、必需文件、互动桥接、分发桥接和 Browser Agent Provider 快照执行链,再输出:
|
||||
|
||||
```text
|
||||
dist/aitoearn-extension-v<version>.zip
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
import { renderToutiaoMarkdown, renderToutiaoPlainText } from '../cose/core/platforms/toutiao.js'
|
||||
import { AUTO_FILL_PLATFORM_IDS } from '../cose/core/platforms/index.js'
|
||||
import {
|
||||
getDeclarativeProviderAction,
|
||||
validateBrowserProviderSnapshot,
|
||||
} from '../cose/core/declarative-browser-provider.js'
|
||||
|
||||
const ALLOWED_API_ORIGINS = new Set([
|
||||
'https://wxapi.frp.it1024.cc',
|
||||
'http://localhost:3002',
|
||||
@@ -5,17 +12,24 @@ const ALLOWED_API_ORIGINS = new Set([
|
||||
|
||||
const BROWSER_AGENT_STORAGE_KEY = 'aitoearn_browser_agent_v1'
|
||||
const BROWSER_AGENT_ALARM_NAME = 'aitoearn-browser-agent-poll'
|
||||
const BROWSER_AGENT_PLATFORM_IDS = [
|
||||
'csdn',
|
||||
'tencentcloud',
|
||||
'aliyun',
|
||||
'qiehao',
|
||||
]
|
||||
const BROWSER_AGENT_PLATFORM_IDS = [...AUTO_FILL_PLATFORM_IDS]
|
||||
.filter(platformId => platformId !== 'wechat')
|
||||
|
||||
const PLATFORM_TAB_PATTERNS = {
|
||||
csdn: ['https://editor.csdn.net/md/*'],
|
||||
juejin: ['https://juejin.cn/*'],
|
||||
jianshu: ['https://www.jianshu.com/*'],
|
||||
zhihu: ['https://zhuanlan.zhihu.com/*', 'https://www.zhihu.com/*'],
|
||||
toutiao: ['https://mp.toutiao.com/*'],
|
||||
baijiahao: ['https://baijiahao.baidu.com/*'],
|
||||
wangyi: ['https://mp.163.com/*'],
|
||||
sohu: ['https://mp.sohu.com/*'],
|
||||
infoq: ['https://xie.infoq.cn/*'],
|
||||
tencentcloud: ['https://cloud.tencent.com/developer/*'],
|
||||
aliyun: ['https://developer.aliyun.com/*'],
|
||||
segmentfault: ['https://segmentfault.com/*'],
|
||||
'51cto': ['https://blog.51cto.com/*'],
|
||||
oschina: ['https://my.oschina.net/*'],
|
||||
qiehao: ['https://om.qq.com/*'],
|
||||
}
|
||||
|
||||
@@ -51,10 +65,25 @@ function normalizeApiBaseUrl(value) {
|
||||
return url.origin
|
||||
}
|
||||
|
||||
function getPlatformAction(platformId, publishMode) {
|
||||
function getPlatformAction(platformId, publishMode, providerConfig) {
|
||||
if (providerConfig !== undefined) {
|
||||
return getDeclarativeProviderAction(providerConfig, platformId, publishMode)
|
||||
}
|
||||
return PLATFORM_ACTIONS[platformId]?.[publishMode] || null
|
||||
}
|
||||
|
||||
function getTaskExecutionPolicy(platformId, publishMode, openedStatus, providerConfig) {
|
||||
if (openedStatus === 'published') return 'report_published'
|
||||
|
||||
const action = getPlatformAction(platformId, publishMode, providerConfig)
|
||||
if (openedStatus === 'draft_saved' && (publishMode === 'draft' || !action)) {
|
||||
return 'report_draft_saved'
|
||||
}
|
||||
if (action) return 'perform_action'
|
||||
if (openedStatus === 'publication_uncertain') return 'report_uncertain'
|
||||
return 'report_filled'
|
||||
}
|
||||
|
||||
function getExtensionVersion() {
|
||||
return chrome.runtime.getManifest().version
|
||||
}
|
||||
@@ -226,38 +255,79 @@ async function hasCurrentPlatformTab(platformId) {
|
||||
return tabs.some(tab => tab.id && !tab.discarded)
|
||||
}
|
||||
|
||||
async function getCurrentPlatformAccount(platformId) {
|
||||
async function inspectCurrentPlatformAccount(platformId) {
|
||||
const response = await chrome.runtime.sendMessage({
|
||||
type: 'GET_PLATFORM_ACCOUNT',
|
||||
platformId,
|
||||
})
|
||||
if (!response?.success || !response.platformUid) return null
|
||||
if (!response?.success || !response.platformUid) {
|
||||
return {
|
||||
account: null,
|
||||
error: response?.error
|
||||
|| (response?.loggedIn
|
||||
? '已检测到登录状态,但平台没有返回稳定账号标识'
|
||||
: '平台登录状态未通过核对'),
|
||||
}
|
||||
}
|
||||
return {
|
||||
platform: platformId,
|
||||
platformUid: String(response.platformUid),
|
||||
displayName: response.displayName || String(response.platformUid),
|
||||
account: {
|
||||
platform: platformId,
|
||||
platformUid: String(response.platformUid),
|
||||
displayName: response.displayName || String(response.platformUid),
|
||||
},
|
||||
error: null,
|
||||
}
|
||||
}
|
||||
|
||||
async function listCurrentPlatformAccounts() {
|
||||
async function getCurrentPlatformAccount(platformId) {
|
||||
return (await inspectCurrentPlatformAccount(platformId)).account
|
||||
}
|
||||
|
||||
async function inspectCurrentPlatformAccounts() {
|
||||
const accounts = []
|
||||
const checks = []
|
||||
for (const platformId of BROWSER_AGENT_PLATFORM_IDS) {
|
||||
if (!await hasCurrentPlatformTab(platformId)) continue
|
||||
try {
|
||||
const account = await getCurrentPlatformAccount(platformId)
|
||||
if (account) accounts.push(account)
|
||||
} catch {}
|
||||
const result = await inspectCurrentPlatformAccount(platformId)
|
||||
if (result.account) {
|
||||
accounts.push(result.account)
|
||||
checks.push({
|
||||
platform: platformId,
|
||||
status: 'verified',
|
||||
message: '账号已核对',
|
||||
})
|
||||
} else {
|
||||
checks.push({
|
||||
platform: platformId,
|
||||
status: 'failed',
|
||||
message: result.error,
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
checks.push({
|
||||
platform: platformId,
|
||||
status: 'failed',
|
||||
message: getMessage(error, '平台账号检测失败'),
|
||||
})
|
||||
}
|
||||
}
|
||||
return accounts
|
||||
return { accounts, checks }
|
||||
}
|
||||
|
||||
async function listCurrentPlatformAccounts() {
|
||||
return (await inspectCurrentPlatformAccounts()).accounts
|
||||
}
|
||||
|
||||
function toTaskContent(task) {
|
||||
const body = typeof task.body === 'string' ? task.body : ''
|
||||
const isHtml = /<[a-z][\w:-]*(?:\s[^<>]*)?>/i.test(body)
|
||||
return {
|
||||
title: typeof task.title === 'string' ? task.title : '',
|
||||
body,
|
||||
html: body,
|
||||
plainText: body.replace(/<[^>]+>/g, ' '),
|
||||
markdown: body,
|
||||
html: isHtml ? body : renderToutiaoMarkdown(body),
|
||||
plainText: isHtml ? body.replace(/<[^>]+>/g, ' ') : renderToutiaoPlainText(body),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -271,8 +341,8 @@ function createTaskResult(task, account, status, extra = {}) {
|
||||
}
|
||||
}
|
||||
|
||||
async function clickPlatformAction(platformId, tabId, publishMode) {
|
||||
const action = getPlatformAction(platformId, publishMode)
|
||||
async function clickPlatformAction(platformId, tabId, publishMode, providerConfig) {
|
||||
const action = getPlatformAction(platformId, publishMode, providerConfig)
|
||||
if (!action) {
|
||||
return {
|
||||
success: false,
|
||||
@@ -336,6 +406,20 @@ async function executeClaimedTask(task) {
|
||||
}
|
||||
}
|
||||
|
||||
let providerConfig
|
||||
try {
|
||||
providerConfig = task.handoff?.providerConfig === undefined
|
||||
? undefined
|
||||
: validateBrowserProviderSnapshot(task.handoff.providerConfig, task.platform)
|
||||
} catch (error) {
|
||||
return {
|
||||
report: true,
|
||||
result: createTaskResult(task, account, 'failed', {
|
||||
message: getMessage(error, '声明式 Browser Provider 配置无效'),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
let opened
|
||||
try {
|
||||
opened = await chrome.runtime.sendMessage({
|
||||
@@ -343,6 +427,7 @@ async function executeClaimedTask(task) {
|
||||
platformId: task.platform,
|
||||
content: toTaskContent(task),
|
||||
expectedPlatformUid: task.platformUid,
|
||||
providerConfig,
|
||||
})
|
||||
} catch (error) {
|
||||
return {
|
||||
@@ -361,7 +446,41 @@ async function executeClaimedTask(task) {
|
||||
}
|
||||
}
|
||||
|
||||
const clickResult = await clickPlatformAction(task.platform, opened.tabId, task.handoff.publishMode)
|
||||
const executionPolicy = getTaskExecutionPolicy(
|
||||
task.platform,
|
||||
task.handoff.publishMode,
|
||||
opened.status,
|
||||
providerConfig,
|
||||
)
|
||||
if (executionPolicy !== 'perform_action') {
|
||||
const status = {
|
||||
report_published: 'published',
|
||||
report_draft_saved: 'draft_saved',
|
||||
report_uncertain: 'publication_uncertain',
|
||||
report_filled: 'filled',
|
||||
}[executionPolicy]
|
||||
const fallbackMessage = status === 'filled'
|
||||
? task.handoff.publishMode === 'publish'
|
||||
? '内容已自动填充,当前平台发布动作尚未完成受控验证,请在已打开的编辑器中人工确认发布'
|
||||
: '内容已自动填充,当前平台草稿动作尚未完成受控验证,请在已打开的编辑器中人工保存草稿'
|
||||
: undefined
|
||||
|
||||
return {
|
||||
report: true,
|
||||
result: createTaskResult(task, account, status, {
|
||||
tabId: opened.tabId,
|
||||
url: opened.url,
|
||||
message: opened.message || fallbackMessage,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
const clickResult = await clickPlatformAction(
|
||||
task.platform,
|
||||
opened.tabId,
|
||||
task.handoff.publishMode,
|
||||
providerConfig,
|
||||
)
|
||||
if (!clickResult.success) {
|
||||
return {
|
||||
report: true,
|
||||
@@ -444,15 +563,18 @@ async function pollBrowserAgentNow() {
|
||||
const agent = await getStoredBrowserAgent()
|
||||
if (!agent) return { success: false, status: 'not_paired' }
|
||||
|
||||
const accounts = await listCurrentPlatformAccounts()
|
||||
const { accounts, checks: accountChecks } = await inspectCurrentPlatformAccounts()
|
||||
if (accounts.length === 0) {
|
||||
const next = await updateRuntimeState(agent, {
|
||||
status: 'waiting_for_platform_account',
|
||||
message: '未找到已打开且可核对的受支持平台账号',
|
||||
message: accountChecks.length > 0
|
||||
? `已打开 ${accountChecks.length} 个受支持平台,但账号核对未通过`
|
||||
: '未找到已打开的受支持平台标签页',
|
||||
lastError: undefined,
|
||||
accounts: [],
|
||||
accountChecks,
|
||||
})
|
||||
return toPublicAgentState(next, { success: true, accounts: [] })
|
||||
return toPublicAgentState(next, { success: true, accounts: [], accountChecks })
|
||||
}
|
||||
|
||||
let claimed
|
||||
@@ -472,8 +594,9 @@ async function pollBrowserAgentNow() {
|
||||
status: 'offline',
|
||||
lastError: getMessage(error, '领取浏览器任务失败'),
|
||||
accounts,
|
||||
accountChecks,
|
||||
})
|
||||
return toPublicAgentState(next, { success: false, accounts })
|
||||
return toPublicAgentState(next, { success: false, accounts, accountChecks })
|
||||
}
|
||||
|
||||
const task = claimed?.task
|
||||
@@ -484,8 +607,9 @@ async function pollBrowserAgentNow() {
|
||||
lastError: undefined,
|
||||
lastClaimAt: new Date().toISOString(),
|
||||
accounts,
|
||||
accountChecks,
|
||||
})
|
||||
return toPublicAgentState(next, { success: true, accounts, task: null })
|
||||
return toPublicAgentState(next, { success: true, accounts, accountChecks, task: null })
|
||||
}
|
||||
|
||||
const execution = await executeClaimedTask(task)
|
||||
@@ -495,8 +619,9 @@ async function pollBrowserAgentNow() {
|
||||
message: execution.message,
|
||||
lastTaskId: task.taskId,
|
||||
accounts,
|
||||
accountChecks,
|
||||
})
|
||||
return toPublicAgentState(next, { success: false, accounts, taskId: task.taskId })
|
||||
return toPublicAgentState(next, { success: false, accounts, accountChecks, taskId: task.taskId })
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -511,10 +636,12 @@ async function pollBrowserAgentNow() {
|
||||
lastTaskId: task.taskId,
|
||||
lastResultAt: new Date().toISOString(),
|
||||
accounts,
|
||||
accountChecks,
|
||||
})
|
||||
return toPublicAgentState(next, {
|
||||
success: true,
|
||||
accounts,
|
||||
accountChecks,
|
||||
taskId: task.taskId,
|
||||
result: execution.result,
|
||||
})
|
||||
@@ -525,8 +652,9 @@ async function pollBrowserAgentNow() {
|
||||
lastError: getMessage(error, '浏览器任务结果回写失败'),
|
||||
lastTaskId: task.taskId,
|
||||
accounts,
|
||||
accountChecks,
|
||||
})
|
||||
return toPublicAgentState(next, { success: false, accounts, taskId: task.taskId })
|
||||
return toPublicAgentState(next, { success: false, accounts, accountChecks, taskId: task.taskId })
|
||||
}
|
||||
})().finally(() => {
|
||||
pollingPromise = null
|
||||
@@ -589,6 +717,12 @@ if (typeof chrome !== 'undefined' && chrome.runtime?.onMessage && chrome.alarms)
|
||||
export {
|
||||
ALLOWED_API_ORIGINS,
|
||||
BROWSER_AGENT_PLATFORM_IDS,
|
||||
PLATFORM_TAB_PATTERNS,
|
||||
executeClaimedTask,
|
||||
getPlatformAction,
|
||||
getTaskExecutionPolicy,
|
||||
inspectCurrentPlatformAccounts,
|
||||
listCurrentPlatformAccounts,
|
||||
normalizeApiBaseUrl,
|
||||
toTaskContent,
|
||||
}
|
||||
|
||||
@@ -6,6 +6,10 @@ import {
|
||||
SYNC_HANDLERS,
|
||||
} from './core/platforms/index.js'
|
||||
import { canReturnExistingPlatformTaskState } from './core/task-state.js'
|
||||
import {
|
||||
syncDeclarativeBrowserProvider,
|
||||
validateBrowserProviderSnapshot,
|
||||
} from './core/declarative-browser-provider.js'
|
||||
import { qianfanIntercept } from './core/platforms/qianfan.js'
|
||||
import { detectSupportedPlatformUser } from './detection/src/supported-platforms.js'
|
||||
// [DISABLED] import { fillAlipayOpenContent } from '@cose/core/src/platforms/alipayopen.js'
|
||||
@@ -263,6 +267,21 @@ async function getCsdnProfileFromSiteContext() {
|
||||
const result = await chrome.scripting.executeScript({
|
||||
target: { tabId: tab.id },
|
||||
func: async () => {
|
||||
const readVisibleProfile = () => {
|
||||
const image = document.querySelector(
|
||||
'a[href*="mp.csdn.net/mp_blog/manage/article"] img[alt]',
|
||||
)
|
||||
const userName = image?.getAttribute('alt')?.trim()
|
||||
if (!userName) return null
|
||||
|
||||
return {
|
||||
userName,
|
||||
nickname: userName,
|
||||
avatarUrl: image.currentSrc || image.src || '',
|
||||
}
|
||||
}
|
||||
|
||||
const visibleProfile = readVisibleProfile()
|
||||
const deadline = Date.now() + 12000
|
||||
while (Date.now() < deadline) {
|
||||
if (typeof window.csdn?.httpGateway === 'function') break
|
||||
@@ -270,6 +289,9 @@ async function getCsdnProfileFromSiteContext() {
|
||||
}
|
||||
|
||||
if (typeof window.csdn?.httpGateway !== 'function') {
|
||||
if (visibleProfile) {
|
||||
return { status: 200, body: { data: visibleProfile }, source: 'editor_dom' }
|
||||
}
|
||||
return { status: 0, error: 'CSDN 编辑器签名请求客户端未加载' }
|
||||
}
|
||||
|
||||
@@ -280,6 +302,14 @@ async function getCsdnProfileFromSiteContext() {
|
||||
)
|
||||
return { status: 200, body }
|
||||
} catch (error) {
|
||||
if (visibleProfile) {
|
||||
return {
|
||||
status: 200,
|
||||
body: { data: visibleProfile },
|
||||
source: 'editor_dom',
|
||||
apiError: error instanceof Error ? error.message : 'CSDN 账号资料请求失败',
|
||||
}
|
||||
}
|
||||
const response = error?.response
|
||||
return {
|
||||
status: response?.status || 0,
|
||||
@@ -543,7 +573,8 @@ async function handleMessage(request, sender) {
|
||||
request.platformId,
|
||||
request.content,
|
||||
request.expectedPlatformUid,
|
||||
request.tabId
|
||||
request.tabId,
|
||||
request.providerConfig
|
||||
)
|
||||
case 'INSPECT_PLATFORM_TASK':
|
||||
return await inspectPlatformTask(
|
||||
@@ -744,7 +775,7 @@ function validateExpectedPlatformAccount(account, expectedPlatformUid) {
|
||||
return null
|
||||
}
|
||||
|
||||
async function openPlatformTask(platformId, content, expectedPlatformUid, existingTabId) {
|
||||
async function openPlatformTask(platformId, content, expectedPlatformUid, existingTabId, providerConfig) {
|
||||
if (!content || typeof content !== 'object') {
|
||||
return { success: false, error: '缺少待填充内容' }
|
||||
}
|
||||
@@ -765,6 +796,15 @@ async function openPlatformTask(platformId, content, expectedPlatformUid, existi
|
||||
const mismatch = validateExpectedPlatformAccount(account, expectedPlatformUid)
|
||||
if (mismatch) return mismatch
|
||||
|
||||
let validatedProviderConfig
|
||||
try {
|
||||
validatedProviderConfig = providerConfig === undefined
|
||||
? undefined
|
||||
: validateBrowserProviderSnapshot(providerConfig, platformId)
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message || '声明式 Browser Provider 配置无效' }
|
||||
}
|
||||
|
||||
const inspect = INSPECT_HANDLERS[platformId]
|
||||
let reusableTabId
|
||||
if (inspect && Number.isInteger(existingTabId) && existingTabId > 0) {
|
||||
@@ -790,6 +830,7 @@ async function openPlatformTask(platformId, content, expectedPlatformUid, existi
|
||||
const result = await syncToPlatform(platformId, content, {
|
||||
active: true,
|
||||
tabId: reusableTabId,
|
||||
providerConfig: validatedProviderConfig,
|
||||
})
|
||||
if (!result?.success) return result
|
||||
if (result.tabId) {
|
||||
@@ -929,6 +970,20 @@ async function syncToPlatform(platformId, content, options = {}) {
|
||||
try {
|
||||
let tab
|
||||
|
||||
if (options.providerConfig) {
|
||||
return await syncDeclarativeBrowserProvider(
|
||||
platform,
|
||||
content,
|
||||
options.providerConfig,
|
||||
options,
|
||||
{
|
||||
chrome,
|
||||
waitForTab,
|
||||
addTabToSyncGroup,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// 检查是否有平台特定的同步处理器
|
||||
const syncHandler = SYNC_HANDLERS[platformId]
|
||||
if (syncHandler) {
|
||||
|
||||
@@ -0,0 +1,365 @@
|
||||
const CONTROLLED_BROWSER_PROVIDER_PLATFORM_IDS = new Set([
|
||||
'csdn',
|
||||
'juejin',
|
||||
'jianshu',
|
||||
'zhihu',
|
||||
'toutiao',
|
||||
'baijiahao',
|
||||
'wangyi',
|
||||
'sohu',
|
||||
'infoq',
|
||||
'tencentcloud',
|
||||
'aliyun',
|
||||
'segmentfault',
|
||||
'51cto',
|
||||
'oschina',
|
||||
'qiehao',
|
||||
])
|
||||
|
||||
const EDITOR_TYPES = new Set(['textarea', 'contenteditable', 'codemirror5'])
|
||||
const CONTENT_FORMATS = new Set(['markdown', 'html', 'plainText'])
|
||||
|
||||
function isRecord(value) {
|
||||
return Boolean(value) && typeof value === 'object' && !Array.isArray(value)
|
||||
}
|
||||
|
||||
function assertExactKeys(value, allowedKeys, label) {
|
||||
const unexpected = Object.keys(value).filter(key => !allowedKeys.includes(key))
|
||||
if (unexpected.length > 0) {
|
||||
throw new Error(`${label}包含不允许的字段:${unexpected.join(', ')}`)
|
||||
}
|
||||
}
|
||||
|
||||
function assertString(value, label, maximumLength, pattern) {
|
||||
if (typeof value !== 'string' || !value.trim() || value.length > maximumLength) {
|
||||
throw new Error(`${label}无效`)
|
||||
}
|
||||
if (pattern && !pattern.test(value)) throw new Error(`${label}格式无效`)
|
||||
return value.trim()
|
||||
}
|
||||
|
||||
function validateSelector(value, label) {
|
||||
return assertString(value, label, 500, /^[^\u0000-\u001f\u007f]+$/)
|
||||
}
|
||||
|
||||
function validateAction(value, label) {
|
||||
if (value === undefined) return undefined
|
||||
if (!isRecord(value)) throw new Error(`${label}无效`)
|
||||
assertExactKeys(value, ['selector', 'exactText'], label)
|
||||
return {
|
||||
selector: validateSelector(value.selector, `${label}选择器`),
|
||||
exactText: assertString(value.exactText, `${label}按钮文字`, 100),
|
||||
}
|
||||
}
|
||||
|
||||
function validateBrowserProviderSnapshot(value, expectedPlatformId) {
|
||||
if (value === undefined) return null
|
||||
if (!isRecord(value)) throw new Error('声明式 Browser Provider 快照无效')
|
||||
assertExactKeys(
|
||||
value,
|
||||
['pluginId', 'version', 'sourceCommit', 'contentHash', 'definition'],
|
||||
'声明式 Browser Provider 快照',
|
||||
)
|
||||
|
||||
const pluginId = assertString(value.pluginId, 'Provider 插件 ID', 100, /^[a-z\d][\w.-]*$/i)
|
||||
const version = assertString(value.version, 'Provider 插件版本', 64)
|
||||
const sourceCommit = assertString(value.sourceCommit, 'Provider 源提交', 40, /^[0-9a-f]{40}$/i)
|
||||
const contentHash = assertString(value.contentHash, 'Provider 内容哈希', 64, /^[0-9a-f]{64}$/i)
|
||||
const definition = value.definition
|
||||
if (!isRecord(definition)) throw new Error('声明式 Browser Provider 定义无效')
|
||||
assertExactKeys(
|
||||
definition,
|
||||
['platform', 'title', 'content', 'draftAction', 'publishAction'],
|
||||
'声明式 Browser Provider 定义',
|
||||
)
|
||||
|
||||
const platform = assertString(definition.platform, 'Provider 平台', 32)
|
||||
if (!CONTROLLED_BROWSER_PROVIDER_PLATFORM_IDS.has(platform) || platform !== expectedPlatformId) {
|
||||
throw new Error('声明式 Browser Provider 平台与任务平台不一致')
|
||||
}
|
||||
|
||||
if (!isRecord(definition.title)) throw new Error('Provider 标题字段无效')
|
||||
assertExactKeys(definition.title, ['selector', 'activationSelector'], 'Provider 标题字段')
|
||||
const title = {
|
||||
selector: validateSelector(definition.title.selector, 'Provider 标题选择器'),
|
||||
}
|
||||
if (definition.title.activationSelector !== undefined) {
|
||||
title.activationSelector = validateSelector(
|
||||
definition.title.activationSelector,
|
||||
'Provider 标题激活选择器',
|
||||
)
|
||||
}
|
||||
|
||||
if (!isRecord(definition.content)) throw new Error('Provider 正文字段无效')
|
||||
assertExactKeys(definition.content, ['selector', 'editorType', 'format'], 'Provider 正文字段')
|
||||
const editorType = assertString(definition.content.editorType, 'Provider 编辑器类型', 32)
|
||||
const format = assertString(definition.content.format, 'Provider 内容格式', 32)
|
||||
if (!EDITOR_TYPES.has(editorType)) throw new Error('Provider 编辑器类型不受支持')
|
||||
if (!CONTENT_FORMATS.has(format)) throw new Error('Provider 内容格式不受支持')
|
||||
|
||||
const normalizedDefinition = {
|
||||
platform,
|
||||
title,
|
||||
content: {
|
||||
selector: validateSelector(definition.content.selector, 'Provider 正文选择器'),
|
||||
editorType,
|
||||
format,
|
||||
},
|
||||
}
|
||||
const draftAction = validateAction(definition.draftAction, 'Provider 草稿动作')
|
||||
const publishAction = validateAction(definition.publishAction, 'Provider 发布动作')
|
||||
if (draftAction) normalizedDefinition.draftAction = draftAction
|
||||
if (publishAction) normalizedDefinition.publishAction = publishAction
|
||||
|
||||
return {
|
||||
pluginId,
|
||||
version,
|
||||
sourceCommit,
|
||||
contentHash,
|
||||
definition: normalizedDefinition,
|
||||
}
|
||||
}
|
||||
|
||||
function getDeclarativeProviderAction(providerConfig, platformId, publishMode) {
|
||||
const validated = validateBrowserProviderSnapshot(providerConfig, platformId)
|
||||
if (!validated) return null
|
||||
const action = publishMode === 'draft'
|
||||
? validated.definition.draftAction
|
||||
: publishMode === 'publish'
|
||||
? validated.definition.publishAction
|
||||
: undefined
|
||||
if (!action) return null
|
||||
return { selector: action.selector, text: action.exactText }
|
||||
}
|
||||
|
||||
async function fillDeclarativeBrowserProvider(definition, content) {
|
||||
const sleep = milliseconds => new Promise(resolve => setTimeout(resolve, milliseconds))
|
||||
const normalizeText = value => String(value || '').replace(/\s+/g, '')
|
||||
|
||||
function findElement(selector) {
|
||||
const candidates = Array.from(document.querySelectorAll(selector))
|
||||
return candidates.find((element) => {
|
||||
const style = window.getComputedStyle?.(element)
|
||||
return !style || (style.display !== 'none' && style.visibility !== 'hidden')
|
||||
}) || null
|
||||
}
|
||||
|
||||
async function waitForElement(selector, timeout = 15000) {
|
||||
const startedAt = Date.now()
|
||||
while (Date.now() - startedAt < timeout) {
|
||||
const element = findElement(selector)
|
||||
if (element) return element
|
||||
await sleep(100)
|
||||
}
|
||||
return findElement(selector)
|
||||
}
|
||||
|
||||
function dispatchValueEvents(element, value, inputType) {
|
||||
try {
|
||||
element.dispatchEvent(new InputEvent('input', {
|
||||
bubbles: true,
|
||||
data: value,
|
||||
inputType,
|
||||
}))
|
||||
} catch {
|
||||
element.dispatchEvent(new Event('input', { bubbles: true }))
|
||||
}
|
||||
element.dispatchEvent(new Event('change', { bubbles: true }))
|
||||
element.dispatchEvent(new Event('blur', { bubbles: true }))
|
||||
}
|
||||
|
||||
function setNativeValue(element, value) {
|
||||
element.focus()
|
||||
const prototype = element instanceof HTMLTextAreaElement
|
||||
? window.HTMLTextAreaElement.prototype
|
||||
: window.HTMLInputElement.prototype
|
||||
const setter = Object.getOwnPropertyDescriptor(prototype, 'value')?.set
|
||||
if (setter) setter.call(element, value)
|
||||
else element.value = value
|
||||
dispatchValueEvents(element, value, 'insertText')
|
||||
}
|
||||
|
||||
function selectContents(element) {
|
||||
const range = document.createRange()
|
||||
range.selectNodeContents(element)
|
||||
const selection = window.getSelection()
|
||||
selection?.removeAllRanges()
|
||||
selection?.addRange(range)
|
||||
}
|
||||
|
||||
function setContentEditableValue(element, value, format, plainText) {
|
||||
element.focus()
|
||||
selectContents(element)
|
||||
const command = format === 'html' ? 'insertHTML' : 'insertText'
|
||||
const inserted = document.execCommand?.(command, false, value)
|
||||
if (!inserted) {
|
||||
if (format === 'html') element.innerHTML = value
|
||||
else element.textContent = value
|
||||
}
|
||||
dispatchValueEvents(element, plainText || value, format === 'html' ? 'insertFromPaste' : 'insertText')
|
||||
}
|
||||
|
||||
function getCodeMirror(element) {
|
||||
return element?.CodeMirror || element?.closest?.('.CodeMirror')?.CodeMirror
|
||||
}
|
||||
|
||||
const title = typeof content.title === 'string' ? content.title : ''
|
||||
if (definition.title.activationSelector) {
|
||||
const activator = await waitForElement(definition.title.activationSelector)
|
||||
if (!activator) return { success: false, error: '未找到 Provider 标题激活控件' }
|
||||
activator.click()
|
||||
await sleep(150)
|
||||
}
|
||||
|
||||
const titleElement = await waitForElement(definition.title.selector)
|
||||
if (!titleElement) return { success: false, error: '未找到 Provider 标题输入框' }
|
||||
if (title) {
|
||||
if (titleElement instanceof HTMLInputElement || titleElement instanceof HTMLTextAreaElement) {
|
||||
setNativeValue(titleElement, title)
|
||||
} else if (titleElement.isContentEditable || titleElement.getAttribute('contenteditable') === 'true') {
|
||||
setContentEditableValue(titleElement, title, 'plainText', title)
|
||||
} else {
|
||||
return { success: false, error: 'Provider 标题选择器没有指向可写控件' }
|
||||
}
|
||||
await sleep(200)
|
||||
const actualTitle = 'value' in titleElement ? titleElement.value : titleElement.textContent
|
||||
if (normalizeText(actualTitle) !== normalizeText(title)) {
|
||||
return { success: false, error: '平台未确认接收 Provider 标题' }
|
||||
}
|
||||
}
|
||||
|
||||
const contentValue = typeof content[definition.content.format] === 'string'
|
||||
? content[definition.content.format]
|
||||
: typeof content.body === 'string'
|
||||
? content.body
|
||||
: ''
|
||||
if (!contentValue) return { success: true, method: 'title-only', textLength: 0, imageCount: 0 }
|
||||
|
||||
const contentElement = await waitForElement(definition.content.selector)
|
||||
if (!contentElement) return { success: false, error: '未找到 Provider 正文编辑器' }
|
||||
|
||||
let actualContent = ''
|
||||
if (definition.content.editorType === 'codemirror5') {
|
||||
const editor = getCodeMirror(contentElement)
|
||||
if (!editor?.setValue || !editor?.getValue) {
|
||||
return { success: false, error: 'Provider 正文选择器没有指向 CodeMirror 5 编辑器' }
|
||||
}
|
||||
editor.setValue(contentValue)
|
||||
editor.focus?.()
|
||||
actualContent = editor.getValue()
|
||||
} else if (definition.content.editorType === 'textarea') {
|
||||
if (!(contentElement instanceof HTMLTextAreaElement || contentElement instanceof HTMLInputElement)) {
|
||||
return { success: false, error: 'Provider 正文选择器没有指向文本输入控件' }
|
||||
}
|
||||
setNativeValue(contentElement, contentValue)
|
||||
actualContent = contentElement.value
|
||||
} else {
|
||||
if (!contentElement.isContentEditable && contentElement.getAttribute('contenteditable') !== 'true') {
|
||||
return { success: false, error: 'Provider 正文选择器没有指向富文本编辑器' }
|
||||
}
|
||||
setContentEditableValue(
|
||||
contentElement,
|
||||
contentValue,
|
||||
definition.content.format,
|
||||
content.plainText,
|
||||
)
|
||||
actualContent = contentElement.innerText || contentElement.textContent || ''
|
||||
}
|
||||
|
||||
await sleep(500)
|
||||
if (definition.content.editorType === 'codemirror5') {
|
||||
actualContent = getCodeMirror(contentElement)?.getValue?.() || ''
|
||||
} else if (definition.content.editorType === 'textarea') {
|
||||
actualContent = contentElement.value || ''
|
||||
} else {
|
||||
actualContent = contentElement.innerText || contentElement.textContent || ''
|
||||
}
|
||||
|
||||
const expectedText = definition.content.editorType === 'contenteditable'
|
||||
? content.plainText || contentValue.replace(/<[^>]+>/g, ' ')
|
||||
: contentValue
|
||||
const normalizedExpected = normalizeText(expectedText)
|
||||
const normalizedActual = normalizeText(actualContent)
|
||||
const minimumLength = Math.min(
|
||||
normalizedExpected.length,
|
||||
Math.min(Math.max(12, Math.floor(normalizedExpected.length * 0.1)), 120),
|
||||
)
|
||||
const expectedPrefix = normalizedExpected.slice(0, Math.min(32, normalizedExpected.length))
|
||||
const expectedImageCount = definition.content.format === 'html'
|
||||
? (contentValue.match(/<img\b/gi) || []).length
|
||||
: 0
|
||||
const actualImageCount = contentElement.querySelectorAll?.('img').length || 0
|
||||
if (
|
||||
normalizedActual.length < minimumLength
|
||||
|| (expectedPrefix.length >= 8 && !normalizedActual.includes(expectedPrefix))
|
||||
|| actualImageCount < expectedImageCount
|
||||
) {
|
||||
return { success: false, error: '平台未确认接收 Provider 正文,请保留编辑器并人工核对' }
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
method: definition.content.editorType,
|
||||
textLength: normalizedActual.length,
|
||||
imageCount: actualImageCount,
|
||||
}
|
||||
}
|
||||
|
||||
async function syncDeclarativeBrowserProvider(platform, content, providerConfig, options, helpers) {
|
||||
const validated = validateBrowserProviderSnapshot(providerConfig, platform.id)
|
||||
let tab
|
||||
if (Number.isInteger(options.tabId) && options.tabId > 0) {
|
||||
try {
|
||||
tab = await helpers.chrome.tabs.update(options.tabId, {
|
||||
url: platform.publishUrl,
|
||||
active: options.active === true,
|
||||
})
|
||||
} catch {}
|
||||
}
|
||||
if (!tab) {
|
||||
tab = await helpers.chrome.tabs.create({
|
||||
url: platform.publishUrl,
|
||||
active: options.active === true,
|
||||
})
|
||||
await helpers.addTabToSyncGroup(tab.id, tab.windowId)
|
||||
}
|
||||
await helpers.waitForTab(tab.id)
|
||||
|
||||
const execution = await helpers.chrome.scripting.executeScript({
|
||||
target: { tabId: tab.id },
|
||||
func: fillDeclarativeBrowserProvider,
|
||||
args: [validated.definition, content],
|
||||
world: 'MAIN',
|
||||
})
|
||||
const result = execution?.[0]?.result
|
||||
if (!result?.success) {
|
||||
return {
|
||||
success: false,
|
||||
message: result?.error || '声明式 Browser Provider 填充失败',
|
||||
tabId: tab.id,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `已使用 ${validated.pluginId}@${validated.version} 填充平台编辑器`,
|
||||
tabId: tab.id,
|
||||
provider: {
|
||||
pluginId: validated.pluginId,
|
||||
version: validated.version,
|
||||
contentHash: validated.contentHash,
|
||||
},
|
||||
metrics: {
|
||||
textLength: result.textLength || 0,
|
||||
imageCount: result.imageCount || 0,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
CONTROLLED_BROWSER_PROVIDER_PLATFORM_IDS,
|
||||
fillDeclarativeBrowserProvider,
|
||||
getDeclarativeProviderAction,
|
||||
syncDeclarativeBrowserProvider,
|
||||
validateBrowserProviderSnapshot,
|
||||
}
|
||||
@@ -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 = {
|
||||
'&': '&',
|
||||
'>': '>',
|
||||
'<': '<',
|
||||
'"': '"',
|
||||
''': "'",
|
||||
' ': ' ',
|
||||
}
|
||||
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*="正文"]',
|
||||
|
||||
@@ -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 = {
|
||||
'&': '&',
|
||||
'>': '>',
|
||||
'<': '<',
|
||||
'"': '"',
|
||||
''': "'",
|
||||
' ': ' ',
|
||||
}
|
||||
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' }
|
||||
|
||||
|
||||
@@ -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,21 +143,43 @@ 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) {
|
||||
const expectedTitle = normalizeText(title)
|
||||
if (!expectedTitle) return true
|
||||
|
||||
const inputTitle = normalizeText(input?.value)
|
||||
const displayTitle = normalizeText(document.querySelector('.article-bar__title-display')?.textContent)
|
||||
return inputTitle === expectedTitle || displayTitle === expectedTitle
|
||||
}
|
||||
|
||||
async function getTitleInput() {
|
||||
const selectors = [
|
||||
'input.article-bar__title--input',
|
||||
'.article-bar__input-box input[placeholder*="标题"]',
|
||||
'input[placeholder*="文章标题"]',
|
||||
'input[name="title"]',
|
||||
'input.title-input',
|
||||
]
|
||||
const visibleInput = findFirst(selectors)
|
||||
if (visibleInput) return visibleInput
|
||||
|
||||
const titleDisplay = findFirst(['.article-bar__title-display'])
|
||||
titleDisplay?.click()
|
||||
return waitForElement(selectors, undefined, 2500)
|
||||
}
|
||||
|
||||
async function fill() {
|
||||
const titleInput = await waitForElement([
|
||||
'.article-bar__title input',
|
||||
'input[placeholder*="标题"]',
|
||||
'input[name="title"]',
|
||||
'input.title-input',
|
||||
])
|
||||
const titleInput = title ? await getTitleInput() : null
|
||||
if (title && !titleInput) return { success: false, error: '未找到 CSDN 标题输入框' }
|
||||
if (titleInput && title) setInputValue(titleInput, title)
|
||||
if (titleInput && title) {
|
||||
setInputValue(titleInput, title)
|
||||
await sleep(200)
|
||||
if (!hasExpectedTitle(titleInput)) return { success: false, error: 'CSDN 未确认接收标题' }
|
||||
}
|
||||
|
||||
if (!contentToFill) return { success: true, method: 'title-only' }
|
||||
|
||||
@@ -175,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) {
|
||||
|
||||
@@ -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 = {
|
||||
'&': '&',
|
||||
'>': '>',
|
||||
'<': '<',
|
||||
'"': '"',
|
||||
''': "'",
|
||||
' ': ' ',
|
||||
}
|
||||
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 未确认接收标题' }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 = {
|
||||
'&': '&',
|
||||
'>': '>',
|
||||
'<': '<',
|
||||
'"': '"',
|
||||
''': "'",
|
||||
' ': ' ',
|
||||
}
|
||||
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' }
|
||||
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 = {
|
||||
'&': '&',
|
||||
'>': '>',
|
||||
'<': '<',
|
||||
'"': '"',
|
||||
''': "'",
|
||||
' ': ' ',
|
||||
}
|
||||
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: '开源中国未确认接收标题' }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 = {
|
||||
'&': '&',
|
||||
'>': '>',
|
||||
'<': '<',
|
||||
'"': '"',
|
||||
''': "'",
|
||||
' ': ' ',
|
||||
}
|
||||
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 {
|
||||
|
||||
@@ -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 = {
|
||||
'&': '&',
|
||||
'>': '>',
|
||||
'<': '<',
|
||||
'"': '"',
|
||||
''': "'",
|
||||
' ': ' ',
|
||||
}
|
||||
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
|
||||
@@ -97,10 +93,43 @@ function inspectTencentCloudTaskState(rawUrl, publicUrl, draftSaved = false) {
|
||||
}
|
||||
}
|
||||
|
||||
function fillTencentCloudContent(title, markdown, plainText) {
|
||||
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 = {
|
||||
'&': '&',
|
||||
'>': '>',
|
||||
'<': '<',
|
||||
'"': '"',
|
||||
''': "'",
|
||||
' ': ' ',
|
||||
}
|
||||
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)
|
||||
}
|
||||
@@ -145,6 +174,66 @@ function fillTencentCloudContent(title, markdown, plainText) {
|
||||
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 getRichEditorSnapshot(editor) {
|
||||
return {
|
||||
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 snapshot = getRichEditorSnapshot(editor)
|
||||
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) {
|
||||
if (!html || typeof DataTransfer === 'undefined' || typeof ClipboardEvent === 'undefined') return false
|
||||
|
||||
const clipboardData = new DataTransfer()
|
||||
clipboardData.setData('text/html', html)
|
||||
clipboardData.setData('text/plain', plainText || contentToFill)
|
||||
editor.focus()
|
||||
selectEditorContents(editor)
|
||||
editor.dispatchEvent(new ClipboardEvent('paste', {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
clipboardData,
|
||||
}))
|
||||
return true
|
||||
}
|
||||
|
||||
function insertRichTextFallback(editor) {
|
||||
editor.focus()
|
||||
selectEditorContents(editor)
|
||||
if (html) document.execCommand?.('insertHTML', false, html)
|
||||
else document.execCommand?.('insertText', false, plainText || contentToFill)
|
||||
editor.dispatchEvent(new InputEvent('input', {
|
||||
bubbles: true,
|
||||
data: plainText || contentToFill,
|
||||
inputType: html ? 'insertFromPaste' : 'insertText',
|
||||
}))
|
||||
editor.dispatchEvent(new Event('change', { bubbles: true }))
|
||||
editor.dispatchEvent(new Event('blur', { bubbles: true }))
|
||||
}
|
||||
|
||||
function getCodeMirror() {
|
||||
const element = document.querySelector('.CodeMirror')
|
||||
return element?.CodeMirror
|
||||
@@ -170,18 +259,47 @@ function fillTencentCloudContent(title, markdown, 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' }
|
||||
|
||||
const richEditor = await waitForElement([
|
||||
'#cdc-rich-editor.tiptap.ProseMirror[contenteditable="true"]',
|
||||
'#cdc-rich-editor.tiptap.ProseMirror',
|
||||
'.tiptap.ProseMirror[contenteditable="true"]',
|
||||
'.ProseMirror[contenteditable="true"]',
|
||||
], 1200)
|
||||
if (richEditor) {
|
||||
pasteRichText(richEditor)
|
||||
await sleep(350)
|
||||
if (!hasExpectedRichText(richEditor)) {
|
||||
insertRichTextFallback(richEditor)
|
||||
await sleep(350)
|
||||
}
|
||||
if (!hasExpectedRichText(richEditor)) {
|
||||
return { success: false, error: '腾讯云开发者社区未确认接收富文本正文' }
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
method: 'rich-text',
|
||||
imageCount: getRichEditorSnapshot(richEditor).imageCount,
|
||||
}
|
||||
}
|
||||
|
||||
await switchToMarkdownEditor()
|
||||
const codeMirror = await waitForElement(['.CodeMirror'])
|
||||
const codeMirror = await waitForElement(['.CodeMirror'], 4500)
|
||||
const editor = codeMirror?.CodeMirror || getCodeMirror()
|
||||
if (editor && typeof editor.setValue === 'function') {
|
||||
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: '腾讯云开发者社区未确认接收正文' }
|
||||
@@ -191,12 +309,12 @@ function fillTencentCloudContent(title, markdown, plainText) {
|
||||
'textarea[placeholder*="正文"]',
|
||||
'.markdown-editor textarea',
|
||||
'.editor-container textarea',
|
||||
], 4000)
|
||||
], 3000)
|
||||
if (!textarea) return { success: false, error: '未找到腾讯云开发者社区 Markdown 编辑器' }
|
||||
|
||||
setInputValue(textarea, contentToFill)
|
||||
await sleep(250)
|
||||
if (normalizeText(textarea.value) !== normalizeText(contentToFill)) {
|
||||
if (normalizeContent(textarea.value) !== normalizeContent(contentToFill)) {
|
||||
return { success: false, error: '腾讯云开发者社区未确认接收正文' }
|
||||
}
|
||||
|
||||
@@ -249,7 +367,7 @@ async function syncTencentCloudContent(tab, content, helpers) {
|
||||
const result = await helpers.chrome.scripting.executeScript({
|
||||
target: { tabId: tab.id },
|
||||
func: fillTencentCloudContent,
|
||||
args: [content.title, markdown, content.plainText || markdown],
|
||||
args: [content.title, markdown, content.html || '', content.plainText || markdown],
|
||||
world: 'MAIN',
|
||||
})
|
||||
const fillResult = result?.[0]?.result
|
||||
|
||||
@@ -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 = {
|
||||
'&': '&',
|
||||
'>': '>',
|
||||
'<': '<',
|
||||
'"': '"',
|
||||
''': "'",
|
||||
' ': ' ',
|
||||
}
|
||||
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
|
||||
|
||||
@@ -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 = {
|
||||
'&': '&',
|
||||
'>': '>',
|
||||
'<': '<',
|
||||
'"': '"',
|
||||
''': "'",
|
||||
' ': ' ',
|
||||
}
|
||||
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: '网易号未确认接收正文,请在编辑器中手动粘贴后再重试',
|
||||
|
||||
@@ -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: '知乎未确认接收正文,请在编辑器中手动粘贴后再重试',
|
||||
|
||||
@@ -13,7 +13,9 @@ function normalizeString(value) {
|
||||
function parseAliyunAccount(response) {
|
||||
const data = response?.data && typeof response.data === 'object' ? response.data : undefined
|
||||
const account = data?.user && typeof data.user === 'object' ? data.user : data
|
||||
const platformUid = normalizeString(account?.userId)
|
||||
const platformUid = normalizeString(account?.uccId)
|
||||
|| normalizeString(account?.ucc_id)
|
||||
|| normalizeString(account?.userId)
|
||||
|| normalizeString(account?.user_id)
|
||||
|| normalizeString(account?.uid)
|
||||
|| normalizeString(account?.aliuid)
|
||||
|
||||
@@ -25,9 +25,9 @@ function parseQiehaoAccount(html) {
|
||||
}
|
||||
|
||||
return {
|
||||
platformUid: readField(html, ['userID', 'userId']),
|
||||
username: readField(html, ['nickName', 'nickname', 'userName', 'name']) || '',
|
||||
avatar: readField(html, ['avatarUrl', 'avatar']) || '',
|
||||
platformUid: readField(html, ['mediaId', 'userID', 'userId']),
|
||||
username: readField(html, ['mediaName', 'nickName', 'nickname', 'userName', 'name']) || '',
|
||||
avatar: readField(html, ['header', 'avatarUrl', 'avatar']) || '',
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ function normalizeString(value) {
|
||||
|
||||
function getCurrentAccountScope(html) {
|
||||
const match = html.match(
|
||||
/"(?:currentUser|userInfo|creatorInfo)"\s*:\s*(\{(?:[^{}]|\{[^{}]*\}){0,4000}\})/,
|
||||
/"(?:session|currentUser|userInfo|creatorInfo)"\s*:\s*(\{(?:[^{}]|\{[^{}]*\}){0,12000}\})/,
|
||||
)
|
||||
return match?.[1] || ''
|
||||
}
|
||||
@@ -29,7 +29,8 @@ function parseTencentCloudAccount(html) {
|
||||
}
|
||||
|
||||
return {
|
||||
platformUid: readField('uin')
|
||||
platformUid: readField('loginUid')
|
||||
|| readField('uin')
|
||||
|| readField('userId')
|
||||
|| readField('user_id')
|
||||
|| readField('uid')
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"name": "AiToEarn - 内容营销助手",
|
||||
"version": "1.18.0",
|
||||
"version": "1.19.6",
|
||||
"description": "AiToEarn 自维护内容扩展:网页采集、账号检测、多平台文章分发与受控人工互动",
|
||||
"permissions": [
|
||||
"activeTab",
|
||||
|
||||
+66
-1
@@ -60,6 +60,11 @@ if ($LASTEXITCODE -ne 0) {
|
||||
throw 'Distribution bridge validation failed'
|
||||
}
|
||||
|
||||
& node (Join-Path $PSScriptRoot 'test-browser-agent.mjs')
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw 'Browser Agent validation failed'
|
||||
}
|
||||
|
||||
$distRoot = Join-Path $repoRoot 'dist'
|
||||
$stageRoot = Join-Path $distRoot "aitoearn-extension-v$version"
|
||||
$archivePath = Join-Path $distRoot "aitoearn-extension-v$version.zip"
|
||||
@@ -89,7 +94,67 @@ foreach ($relativePath in $requiredPaths) {
|
||||
}
|
||||
}
|
||||
|
||||
Compress-Archive -Path (Join-Path $stageRoot '*') -DestinationPath $archivePath -CompressionLevel Optimal
|
||||
Add-Type -AssemblyName System.IO.Compression
|
||||
|
||||
$archiveStream = $null
|
||||
$archive = $null
|
||||
try {
|
||||
$archiveStream = [System.IO.File]::Open(
|
||||
$archivePath,
|
||||
[System.IO.FileMode]::CreateNew,
|
||||
[System.IO.FileAccess]::ReadWrite,
|
||||
[System.IO.FileShare]::None
|
||||
)
|
||||
$archive = [System.IO.Compression.ZipArchive]::new(
|
||||
$archiveStream,
|
||||
[System.IO.Compression.ZipArchiveMode]::Create,
|
||||
$false,
|
||||
[System.Text.Encoding]::UTF8
|
||||
)
|
||||
$fixedTimestamp = [System.DateTimeOffset]::new(2000, 1, 1, 0, 0, 0, [System.TimeSpan]::Zero)
|
||||
$stageRootFullPath = [System.IO.Path]::GetFullPath($stageRoot)
|
||||
$archiveFiles = Get-ChildItem -LiteralPath $stageRoot -Recurse -File |
|
||||
ForEach-Object {
|
||||
[pscustomobject]@{
|
||||
EntryName = $_.FullName.Substring($stageRootFullPath.Length + 1).Replace('\', '/')
|
||||
SourcePath = $_.FullName
|
||||
}
|
||||
} |
|
||||
Sort-Object -Property EntryName
|
||||
|
||||
foreach ($archiveFile in $archiveFiles) {
|
||||
$entry = $archive.CreateEntry(
|
||||
$archiveFile.EntryName,
|
||||
[System.IO.Compression.CompressionLevel]::Optimal
|
||||
)
|
||||
$entry.LastWriteTime = $fixedTimestamp
|
||||
$entry.ExternalAttributes = 0
|
||||
|
||||
$sourceStream = $null
|
||||
$entryStream = $null
|
||||
try {
|
||||
$sourceStream = [System.IO.File]::OpenRead($archiveFile.SourcePath)
|
||||
$entryStream = $entry.Open()
|
||||
$sourceStream.CopyTo($entryStream)
|
||||
}
|
||||
finally {
|
||||
if ($null -ne $entryStream) {
|
||||
$entryStream.Dispose()
|
||||
}
|
||||
if ($null -ne $sourceStream) {
|
||||
$sourceStream.Dispose()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
finally {
|
||||
if ($null -ne $archive) {
|
||||
$archive.Dispose()
|
||||
}
|
||||
if ($null -ne $archiveStream) {
|
||||
$archiveStream.Dispose()
|
||||
}
|
||||
}
|
||||
|
||||
$verificationRoot = Join-Path $distRoot '.verify'
|
||||
if (Test-Path -LiteralPath $verificationRoot) {
|
||||
|
||||
@@ -3,9 +3,19 @@ import { readFile } from 'node:fs/promises'
|
||||
import {
|
||||
ALLOWED_API_ORIGINS,
|
||||
BROWSER_AGENT_PLATFORM_IDS,
|
||||
PLATFORM_TAB_PATTERNS,
|
||||
executeClaimedTask,
|
||||
getPlatformAction,
|
||||
getTaskExecutionPolicy,
|
||||
inspectCurrentPlatformAccounts,
|
||||
listCurrentPlatformAccounts,
|
||||
normalizeApiBaseUrl,
|
||||
toTaskContent,
|
||||
} from '../distribution/browser-agent/index.js'
|
||||
import {
|
||||
getDeclarativeProviderAction,
|
||||
validateBrowserProviderSnapshot,
|
||||
} from '../distribution/cose/core/declarative-browser-provider.js'
|
||||
|
||||
assert.deepEqual(
|
||||
ALLOWED_API_ORIGINS,
|
||||
@@ -24,7 +34,27 @@ assert.throws(
|
||||
() => normalizeApiBaseUrl('https://wxapi.frp.it1024.cc/v2'),
|
||||
/已配置的 AiToEarn API 地址/,
|
||||
)
|
||||
assert.deepEqual(BROWSER_AGENT_PLATFORM_IDS, ['csdn', 'tencentcloud', 'aliyun', 'qiehao'])
|
||||
assert.deepEqual(BROWSER_AGENT_PLATFORM_IDS, [
|
||||
'csdn',
|
||||
'juejin',
|
||||
'jianshu',
|
||||
'zhihu',
|
||||
'toutiao',
|
||||
'baijiahao',
|
||||
'wangyi',
|
||||
'sohu',
|
||||
'infoq',
|
||||
'tencentcloud',
|
||||
'aliyun',
|
||||
'segmentfault',
|
||||
'51cto',
|
||||
'oschina',
|
||||
'qiehao',
|
||||
])
|
||||
assert.equal(
|
||||
BROWSER_AGENT_PLATFORM_IDS.every(platformId => PLATFORM_TAB_PATTERNS[platformId]?.length > 0),
|
||||
true,
|
||||
)
|
||||
assert.deepEqual(getPlatformAction('qiehao', 'draft'), {
|
||||
selector: 'button.omui-button.omui-button--background-grey',
|
||||
text: '存草稿',
|
||||
@@ -35,6 +65,289 @@ assert.deepEqual(getPlatformAction('qiehao', 'publish'), {
|
||||
})
|
||||
assert.equal(getPlatformAction('qiehao', 'unknown'), null)
|
||||
|
||||
const qiehaoProviderSnapshot = {
|
||||
pluginId: 'browser-providers',
|
||||
version: '1.0.0',
|
||||
sourceCommit: 'a'.repeat(40),
|
||||
contentHash: 'b'.repeat(64),
|
||||
definition: {
|
||||
platform: 'qiehao',
|
||||
title: { selector: '.title' },
|
||||
content: {
|
||||
selector: '.ProseMirror',
|
||||
editorType: 'contenteditable',
|
||||
format: 'html',
|
||||
},
|
||||
draftAction: { selector: 'button.plugin-draft', exactText: '保存草稿' },
|
||||
},
|
||||
}
|
||||
assert.deepEqual(getDeclarativeProviderAction(qiehaoProviderSnapshot, 'qiehao', 'draft'), {
|
||||
selector: 'button.plugin-draft',
|
||||
text: '保存草稿',
|
||||
})
|
||||
assert.deepEqual(getPlatformAction('qiehao', 'draft', qiehaoProviderSnapshot), {
|
||||
selector: 'button.plugin-draft',
|
||||
text: '保存草稿',
|
||||
})
|
||||
assert.throws(
|
||||
() => validateBrowserProviderSnapshot({
|
||||
...qiehaoProviderSnapshot,
|
||||
definition: {
|
||||
...qiehaoProviderSnapshot.definition,
|
||||
editorUrl: 'https://attacker.example/editor',
|
||||
},
|
||||
}, 'qiehao'),
|
||||
/不允许的字段/,
|
||||
)
|
||||
assert.throws(
|
||||
() => validateBrowserProviderSnapshot(qiehaoProviderSnapshot, 'csdn'),
|
||||
/任务平台不一致/,
|
||||
)
|
||||
assert.equal(getTaskExecutionPolicy('qiehao', 'draft', 'draft_saved'), 'report_draft_saved')
|
||||
assert.equal(getTaskExecutionPolicy('qiehao', 'draft', 'filled'), 'perform_action')
|
||||
assert.equal(getTaskExecutionPolicy('qiehao', 'publish', 'draft_saved'), 'perform_action')
|
||||
assert.equal(getTaskExecutionPolicy('juejin', 'draft', 'draft_saved'), 'report_draft_saved')
|
||||
assert.equal(getTaskExecutionPolicy('juejin', 'draft', 'filled'), 'report_filled')
|
||||
assert.equal(getTaskExecutionPolicy('juejin', 'publish', 'draft_saved'), 'report_draft_saved')
|
||||
assert.equal(getTaskExecutionPolicy('juejin', 'publish', 'publication_uncertain'), 'report_uncertain')
|
||||
assert.equal(getTaskExecutionPolicy('juejin', 'publish', 'published'), 'report_published')
|
||||
|
||||
const sentMessages = []
|
||||
let scriptExecutionCount = 0
|
||||
globalThis.chrome = {
|
||||
runtime: {
|
||||
getManifest: () => ({ version: '1.18.3' }),
|
||||
sendMessage: async (message) => {
|
||||
sentMessages.push(message)
|
||||
if (message.type === 'GET_PLATFORM_ACCOUNT') {
|
||||
return {
|
||||
success: true,
|
||||
platformUid: `${message.platformId}-uid`,
|
||||
displayName: `${message.platformId} account`,
|
||||
}
|
||||
}
|
||||
if (message.type === 'OPEN_PLATFORM_TASK') {
|
||||
return {
|
||||
success: true,
|
||||
status: 'filled',
|
||||
tabId: 101,
|
||||
url: 'https://juejin.cn/editor/drafts/new',
|
||||
message: '掘金编辑器已填充',
|
||||
}
|
||||
}
|
||||
throw new Error(`Unexpected message: ${message.type}`)
|
||||
},
|
||||
},
|
||||
tabs: {
|
||||
query: async ({ url }) => url?.length ? [{ id: 1, discarded: false }] : [],
|
||||
},
|
||||
scripting: {
|
||||
executeScript: async () => {
|
||||
scriptExecutionCount += 1
|
||||
return []
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const listedAccounts = await listCurrentPlatformAccounts()
|
||||
assert.equal(listedAccounts.length, BROWSER_AGENT_PLATFORM_IDS.length)
|
||||
assert.deepEqual(
|
||||
listedAccounts.map(account => account.platform),
|
||||
BROWSER_AGENT_PLATFORM_IDS,
|
||||
)
|
||||
|
||||
const originalAccountMessageHandler = globalThis.chrome.runtime.sendMessage
|
||||
const originalTabQueryHandler = globalThis.chrome.tabs.query
|
||||
globalThis.chrome.tabs.query = async ({ url }) => {
|
||||
const patterns = Array.isArray(url) ? url : []
|
||||
return patterns.some(pattern => pattern.includes('csdn') || pattern.includes('aliyun'))
|
||||
? [{ id: 1, discarded: false }]
|
||||
: []
|
||||
}
|
||||
globalThis.chrome.runtime.sendMessage = async (message) => {
|
||||
if (message.type !== 'GET_PLATFORM_ACCOUNT') {
|
||||
throw new Error(`Unexpected message: ${message.type}`)
|
||||
}
|
||||
if (message.platformId === 'csdn') {
|
||||
return {
|
||||
success: true,
|
||||
platformUid: 'csdn-uid',
|
||||
displayName: 'CSDN account',
|
||||
}
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
loggedIn: true,
|
||||
error: '已检测到登录状态,但平台没有返回稳定账号 UID',
|
||||
}
|
||||
}
|
||||
|
||||
const inspectedAccounts = await inspectCurrentPlatformAccounts()
|
||||
assert.deepEqual(inspectedAccounts.accounts, [{
|
||||
platform: 'csdn',
|
||||
platformUid: 'csdn-uid',
|
||||
displayName: 'CSDN account',
|
||||
}])
|
||||
assert.deepEqual(inspectedAccounts.checks, [
|
||||
{
|
||||
platform: 'csdn',
|
||||
status: 'verified',
|
||||
message: '账号已核对',
|
||||
},
|
||||
{
|
||||
platform: 'aliyun',
|
||||
status: 'failed',
|
||||
message: '已检测到登录状态,但平台没有返回稳定账号 UID',
|
||||
},
|
||||
])
|
||||
|
||||
globalThis.chrome.runtime.sendMessage = originalAccountMessageHandler
|
||||
globalThis.chrome.tabs.query = originalTabQueryHandler
|
||||
|
||||
sentMessages.length = 0
|
||||
const juejinProviderSnapshot = {
|
||||
...qiehaoProviderSnapshot,
|
||||
definition: {
|
||||
platform: 'juejin',
|
||||
title: { selector: '.title-input' },
|
||||
content: {
|
||||
selector: '.CodeMirror',
|
||||
editorType: 'codemirror5',
|
||||
format: 'markdown',
|
||||
},
|
||||
},
|
||||
}
|
||||
const filledExecution = await executeClaimedTask({
|
||||
platform: 'juejin',
|
||||
platformUid: 'juejin-uid',
|
||||
title: '后台交接测试',
|
||||
body: '# 正文',
|
||||
handoff: {
|
||||
publishMode: 'draft',
|
||||
expiresAt: new Date(Date.now() + 60_000).toISOString(),
|
||||
providerConfig: juejinProviderSnapshot,
|
||||
},
|
||||
})
|
||||
assert.equal(filledExecution.report, true)
|
||||
assert.equal(filledExecution.result.status, 'filled')
|
||||
assert.equal(filledExecution.result.platformUid, 'juejin-uid')
|
||||
assert.equal(filledExecution.result.tabId, 101)
|
||||
assert.equal(scriptExecutionCount, 0)
|
||||
assert.deepEqual(sentMessages.map(message => message.type), [
|
||||
'GET_PLATFORM_ACCOUNT',
|
||||
'OPEN_PLATFORM_TASK',
|
||||
])
|
||||
assert.deepEqual(
|
||||
sentMessages.find(message => message.type === 'OPEN_PLATFORM_TASK')?.providerConfig,
|
||||
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({
|
||||
title: '浏览器交接测试',
|
||||
body: '# 标题\n\n- 列表项\n\n',
|
||||
})
|
||||
assert.equal(markdownTaskContent.markdown, '# 标题\n\n- 列表项\n\n')
|
||||
assert.match(markdownTaskContent.html, /<h1>标题<\/h1>/)
|
||||
assert.match(markdownTaskContent.html, /<ul><li>列表项<\/li><\/ul>/)
|
||||
assert.match(markdownTaskContent.html, /<img src="https:\/\/example\.com\/image\.png" alt="配图" \/>/)
|
||||
assert.equal(markdownTaskContent.plainText.includes('#'), false)
|
||||
|
||||
const htmlTaskContent = toTaskContent({
|
||||
title: '富文本交接测试',
|
||||
body: '<p>正文</p><img src="https://example.com/image.png" alt="配图" />',
|
||||
})
|
||||
assert.equal(htmlTaskContent.html, '<p>正文</p><img src="https://example.com/image.png" alt="配图" />')
|
||||
assert.match(htmlTaskContent.plainText, /正文/)
|
||||
assert.equal(htmlTaskContent.plainText.includes('<img'), false)
|
||||
|
||||
const agentSource = await readFile(
|
||||
new URL('../distribution/browser-agent/index.js', import.meta.url),
|
||||
'utf8',
|
||||
|
||||
@@ -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(/</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',
|
||||
@@ -489,6 +956,21 @@ assert.deepEqual(
|
||||
avatar: 'https://example.com/aliyun-avatar.jpg',
|
||||
},
|
||||
)
|
||||
assert.deepEqual(
|
||||
parseAliyunAccount({
|
||||
success: true,
|
||||
data: {
|
||||
uccId: 'aliyun-ucc-123456',
|
||||
nickname: '阿里云当前作者',
|
||||
avatar: 'https://example.com/aliyun-current-avatar.jpg',
|
||||
},
|
||||
}),
|
||||
{
|
||||
platformUid: 'aliyun-ucc-123456',
|
||||
username: '阿里云当前作者',
|
||||
avatar: 'https://example.com/aliyun-current-avatar.jpg',
|
||||
},
|
||||
)
|
||||
assert.equal(
|
||||
parseAliyunAccount({ success: true, data: { nickname: '无稳定标识' } }).platformUid,
|
||||
undefined,
|
||||
@@ -647,6 +1129,16 @@ assert.deepEqual(
|
||||
avatar: 'https://example.com/tencent-avatar.jpg',
|
||||
},
|
||||
)
|
||||
assert.deepEqual(
|
||||
parseTencentCloudAccount(
|
||||
'<script>{"session":{"isLogined":true,"loginUid":7677118,"userInfo":{"nickname":"用户7677118","avatarUrl":"https://example.com/tencent-current-avatar.jpg"}}}</script>',
|
||||
),
|
||||
{
|
||||
platformUid: '7677118',
|
||||
username: '用户7677118',
|
||||
avatar: 'https://example.com/tencent-current-avatar.jpg',
|
||||
},
|
||||
)
|
||||
assert.equal(
|
||||
parseTencentCloudAccount('<script>{"creatorInfo":{"nickname":"无稳定标识"}}</script>').platformUid,
|
||||
undefined,
|
||||
@@ -685,6 +1177,14 @@ assert.equal(
|
||||
assert.equal(typeof SYNC_HANDLERS.tencentcloud, 'function')
|
||||
assert.equal(typeof INSPECT_HANDLERS.tencentcloud, 'function')
|
||||
|
||||
const tencentCloudPlatformSource = await readFile(
|
||||
new URL('../distribution/cose/core/platforms/tencentcloud.js', import.meta.url),
|
||||
'utf8',
|
||||
)
|
||||
assert.match(tencentCloudPlatformSource, /#cdc-rich-editor\.tiptap\.ProseMirror/)
|
||||
assert.match(tencentCloudPlatformSource, /method: 'rich-text'/)
|
||||
assert.match(tencentCloudPlatformSource, /args: \[content\.title, markdown, content\.html \|\| '', content\.plainText \|\| markdown\]/)
|
||||
|
||||
const tencentCloudDetectorSource = await readFile(
|
||||
new URL('../distribution/cose/detection/src/platforms/tencentcloud.js', import.meta.url),
|
||||
'utf8',
|
||||
@@ -701,6 +1201,16 @@ assert.deepEqual(
|
||||
avatar: 'https://example.com/qiehao-avatar.jpg',
|
||||
},
|
||||
)
|
||||
assert.deepEqual(
|
||||
parseQiehaoAccount(
|
||||
'<script>{"topInfo":{"mediaId":24973524,"mediaName":"企鹅号创作者973524","header":"https://example.com/qiehao-current-avatar.jpg"}}</script>',
|
||||
),
|
||||
{
|
||||
platformUid: '24973524',
|
||||
username: '企鹅号创作者973524',
|
||||
avatar: 'https://example.com/qiehao-current-avatar.jpg',
|
||||
},
|
||||
)
|
||||
assert.equal(
|
||||
parseQiehaoAccount('<script>{"nickName":"没有稳定标识"}</script>').platformUid,
|
||||
undefined,
|
||||
@@ -745,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',
|
||||
@@ -871,6 +1569,9 @@ const csdnPlatformSource = await readFile(
|
||||
)
|
||||
assert.match(csdnPlatformSource, /syncCsdnContent/)
|
||||
assert.match(csdnPlatformSource, /fillCsdnContent/)
|
||||
assert.match(csdnPlatformSource, /\.article-bar__title-display/)
|
||||
assert.match(csdnPlatformSource, /input\.article-bar__title--input/)
|
||||
assert.match(csdnPlatformSource, /CSDN 未确认接收标题/)
|
||||
|
||||
const segmentFaultPlatformSource = await readFile(
|
||||
new URL('../distribution/cose/core/platforms/segmentfault.js', import.meta.url),
|
||||
@@ -891,6 +1592,13 @@ const csdnDetectorSource = await readFile(
|
||||
)
|
||||
assert.doesNotMatch(csdnDetectorSource, /chrome\.cookies|Cookie\s*:/)
|
||||
|
||||
const coseBackgroundSource = await readFile(
|
||||
new URL('../distribution/cose/background.js', import.meta.url),
|
||||
'utf8',
|
||||
)
|
||||
assert.match(coseBackgroundSource, /mp\.csdn\.net\/mp_blog\/manage\/article/)
|
||||
assert.match(coseBackgroundSource, /source: 'editor_dom'/)
|
||||
|
||||
const segmentFaultDetectorSource = await readFile(
|
||||
new URL('../distribution/cose/detection/src/platforms/segmentfault.js', import.meta.url),
|
||||
'utf8',
|
||||
@@ -952,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: {
|
||||
@@ -1015,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({
|
||||
@@ -1079,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',
|
||||
|
||||
Reference in New Issue
Block a user