Repository: doocs/cose Branch: main Commit: 4e32015064b3 Files: 90 Total size: 372.0 KB Directory structure: gitextract_uy5ndwh6/ ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── 01-bug-report.yml │ │ ├── 02-feature-request.yml │ │ └── 03-platform-request.yml │ └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── PRIVACY.md ├── README.md ├── apps/ │ └── extension/ │ ├── manifest.json │ ├── package.json │ ├── scripts/ │ │ ├── cli.ts │ │ ├── convert-icons.mjs │ │ └── reload-extension.mjs │ ├── src/ │ │ ├── background.js │ │ ├── content.js │ │ ├── inject.js │ │ ├── offscreen.html │ │ ├── offscreen.js │ │ ├── popup.html │ │ └── popup.js │ ├── tsconfig.json │ └── vite.config.js ├── package.json ├── packages/ │ ├── core/ │ │ ├── index.js │ │ ├── package.json │ │ └── src/ │ │ ├── platforms/ │ │ │ ├── alipayopen.js │ │ │ ├── aliyun.js │ │ │ ├── baijiahao.js │ │ │ ├── bilibili.js │ │ │ ├── cnblogs.js │ │ │ ├── common.js │ │ │ ├── csdn.js │ │ │ ├── cto51.js │ │ │ ├── douban.js │ │ │ ├── douyin.js │ │ │ ├── elecfans.js │ │ │ ├── huaweicloud.js │ │ │ ├── huaweidev.js │ │ │ ├── index.js │ │ │ ├── infoq.js │ │ │ ├── jianshu.js │ │ │ ├── juejin.js │ │ │ ├── medium.js │ │ │ ├── modelscope.js │ │ │ ├── oschina.js │ │ │ ├── qianfan.js │ │ │ ├── segmentfault.js │ │ │ ├── sohu.js │ │ │ ├── sspai.js │ │ │ ├── tencentcloud.js │ │ │ ├── toutiao.js │ │ │ ├── twitter.js │ │ │ ├── volcengine.js │ │ │ ├── wangyihao.js │ │ │ ├── wechat.js │ │ │ ├── weibo.js │ │ │ ├── xiaohongshu.js │ │ │ └── zhihu.js │ │ └── utils.js │ └── detection/ │ ├── index.js │ ├── package.json │ └── src/ │ ├── configs.js │ ├── detect.js │ ├── platforms/ │ │ ├── alipay.js │ │ ├── aliyun.js │ │ ├── bilibili.js │ │ ├── cnblogs.js │ │ ├── csdn.js │ │ ├── cto51.js │ │ ├── douban.js │ │ ├── elecfans.js │ │ ├── huaweicloud.js │ │ ├── huaweidev.js │ │ ├── infoq.js │ │ ├── jianshu.js │ │ ├── medium.js │ │ ├── modelscope.js │ │ ├── oschina.js │ │ ├── qianfan.js │ │ ├── segmentfault.js │ │ ├── sohu.js │ │ ├── sspai.js │ │ ├── tencentcloud.js │ │ ├── twitter.js │ │ ├── volcengine.js │ │ ├── wangyihao.js │ │ ├── wechat.js │ │ ├── weibo.js │ │ └── xiaohongshu.js │ └── utils.js └── pnpm-workspace.yaml ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/ISSUE_TEMPLATE/01-bug-report.yml ================================================ name: Bug Report / 问题报告 description: Report a bug or issue with the extension / 报告扩展的 bug 或问题 title: "[Bug]: " labels: ["bug", "triage"] body: - type: markdown attributes: value: | Thanks for taking the time to report this bug! Please fill out the information below to help us resolve the issue. 感谢您花时间报告此问题!请填写以下信息以帮助我们解决问题。 - type: textarea id: description attributes: label: Description / 问题描述 description: A clear and concise description of the bug / 清晰简洁地描述问题 placeholder: Describe the bug you encountered / 描述您遇到的问题 validations: required: true - type: textarea id: reproduction-steps attributes: label: Steps to Reproduce / 复现步骤 description: Detailed steps to reproduce the behavior / 详细的复现步骤 placeholder: | 1. Go to '...' / 前往 '...' 2. Click on '...' / 点击 '...' 3. Scroll down to '...' / 滚动到 '...' 4. See error / 看到错误 validations: required: true - type: textarea id: expected-behavior attributes: label: Expected Behavior / 预期行为 description: What you expected to happen / 您期望发生什么 placeholder: Describe the expected behavior / 描述预期行为 validations: required: true - type: textarea id: suggested-implementation attributes: label: Suggested Implementation / 实现建议 description: Any suggestions on how to fix this issue (optional) / 对如何修复此问题的建议 placeholder: Describe your suggested implementation / 描述您建议的实现方式 validations: required: false - type: input id: platform attributes: label: Platform / 平台 description: Which platform is affected? / 哪个平台受到影响? placeholder: e.g., CSDN, Zhihu, Juejin, Toutiao / 如:CSDN、知乎、掘金、头条 validations: required: true - type: dropdown id: browser attributes: label: Browser / 浏览器 description: Which browser are you using? / 您使用的是哪个浏览器? options: - Chrome - Firefox - Edge - Safari - Other / 其他 validations: required: true - type: input id: extension-version attributes: label: Extension Version / 扩展版本 description: What version of the extension are you running? / 您运行的扩展版本是多少? placeholder: e.g., 1.2.0 / 如:1.2.0 validations: required: true - type: textarea id: environment attributes: label: Environment Details / 环境信息 description: Any additional environment information / 其他环境信息 placeholder: | - OS / 操作系统: [e.g., Windows 10, macOS 13.0] - Browser Version / 浏览器版本: [e.g., Chrome 120.0] - Additional context / 其他信息 validations: required: false - type: textarea id: screenshots attributes: label: Screenshots / 截图 description: If applicable, add screenshots to help explain your problem / 如有需要,请添加截图帮助说明问题 placeholder: Drag and drop images here / 拖放图片到这里 validations: required: false - type: textarea id: additional-context attributes: label: Additional Context / 补充信息 description: Any other context about the problem / 关于此问题的其他补充信息 validations: required: false ================================================ FILE: .github/ISSUE_TEMPLATE/02-feature-request.yml ================================================ name: Feature Request / 功能请求 description: Suggest a new feature or enhancement / 建议新功能或改进 title: "[Feature]: " labels: ["enhancement"] body: - type: markdown attributes: value: | Thanks for your interest in improving this project! Please describe your feature request below. 感谢您对改进本项目的兴趣!请在下方描述您的功能请求。 - type: textarea id: problem-description attributes: label: Problem Description / 问题描述 description: Is your feature request related to a problem? Describe what the problem is / 您的功能请求是否与某个问题相关?请描述问题 placeholder: "I'm frustrated when... / 我感到困扰的是..." validations: required: true - type: textarea id: proposed-solution attributes: label: Proposed Solution / 建议的解决方案 description: Describe the solution you'd like to see / 描述您希望看到的解决方案 placeholder: A clear and concise description of what you want to happen / 清晰简洁地描述您希望实现的功能 validations: required: true - type: textarea id: alternatives attributes: label: Alternatives Considered / 考虑过的替代方案 description: Describe any alternative solutions or features you've considered / 描述您考虑过的其他解决方案或功能 placeholder: What other approaches could solve this problem? / 还有哪些方法可以解决这个问题? validations: required: false - type: dropdown id: feature-type attributes: label: Feature Type / 功能类型 description: What type of feature is this? / 这是什么类型的功能? options: - New platform support / 新平台支持 - UI/UX improvement / 界面/体验改进 - Performance enhancement / 性能优化 - Content formatting / 内容格式化 - Authentication/Login / 认证/登录 - Other / 其他 validations: required: true - type: textarea id: additional-context attributes: label: Additional Context / 补充信息 description: Add any other context, screenshots, or examples about the feature request / 添加其他相关信息、截图或示例 placeholder: Any mockups, examples, or additional details / 任何原型图、示例或其他细节 validations: required: false - type: checkboxes id: willingness attributes: label: Implementation / 实现意愿 description: Would you be willing to help implement this feature? / 您是否愿意帮助实现此功能? options: - label: I'm willing to submit a PR for this feature / 我愿意为此功能提交 PR required: false - label: I'd prefer to wait for community implementation / 我希望等待社区的实现 required: false ================================================ FILE: .github/ISSUE_TEMPLATE/03-platform-request.yml ================================================ name: Platform Request / 平台支持 description: Request support for a new platform / 请求支持新平台 title: "[Platform]: " labels: ["enhancement", "new platform"] body: - type: input id: platform-name attributes: label: Platform Name / 平台名称 description: Name of the platform you want supported / 您希望支持的平台名称 placeholder: e.g., 简书、博客园 / e.g., Jianshu, cnblogs validations: required: true - type: input id: platform-url attributes: label: Platform URL / 平台网址 description: Main URL of the platform / 平台的主要网址 placeholder: e.g., https://www.jianshu.com validations: required: true - type: textarea id: reason attributes: label: Why this platform? / 为什么需要支持此平台? description: Brief reason for supporting this platform / 简要说明支持此平台的原因 placeholder: e.g., Popular blogging platform in China / 如:国内热门博客平台 validations: required: false - type: checkboxes id: willingness attributes: label: Implementation / 实现意愿 description: Would you be willing to help implement this feature? / 您是否愿意帮助实现此功能? options: - label: I'm willing to submit a PR for this feature / 我愿意为此功能提交 PR required: false - label: I'd prefer to wait for community implementation / 我希望等待社区的实现 required: false ================================================ FILE: .github/PULL_REQUEST_TEMPLATE.md ================================================ ## Summary / 概述 ## Related Issue / 关联 Issue Closes # ## Type of Change / 更改类型 - [ ] Bug fix / 修复 Bug (non-breaking change that fixes an issue / 修复问题的非破坏性更改) - [ ] New feature / 新功能 (non-breaking change that adds functionality / 添加功能的非破坏性更改) - [ ] Breaking change / 破坏性更改 (fix or feature that would cause existing functionality to not work as expected / 会导致现有功能无法正常工作的修复或功能) - [ ] Documentation update / 文档更新 - [ ] Performance improvement / 性能优化 - [ ] Code refactoring / 代码重构 - [ ] Other / 其他 (please describe / 请描述): ## Changes Made / 更改内容 - - - ## Implementation Details / 实现细节 **Key Changes / 主要更改:** - **Technical Notes / 技术说明:** - ## Testing / 测试 ### Testing Checklist / 测试清单 - [ ] I have tested this code locally / 我已在本地测试此代码 - [ ] All existing tests pass / 所有现有测试通过 - [ ] I have added tests for new functionality / 我已为新功能添加测试 - [ ] I have tested on the affected platform(s) / 我已在受影响的平台上测试 - [ ] I have verified the changes work in the target browser(s) / 我已验证更改在目标浏览器中有效 ### Manual Testing Steps / 手动测试步骤 1. 2. 3. ## Screenshots/Videos / 截图/视频 ## Reviewer Checklist / 审阅者清单 - [ ] Code follows the project's style guidelines / 代码遵循项目的风格指南 - [ ] Changes are well-documented / 更改有良好的文档说明 - [ ] No breaking changes or clearly documented if present / 无破坏性更改,或已清楚记录 - [ ] Security implications have been considered / 已考虑安全影响 - [ ] Performance impact has been evaluated / 已评估性能影响 - [ ] All discussions have been resolved / 所有讨论已解决 ## Additional Notes / 补充说明 ================================================ FILE: .gitignore ================================================ # misc .DS_Store *.zip .vscode .idea .cursor .fleet .zed .windsurf .kiro dist/ node_modules/ ================================================ FILE: PRIVACY.md ================================================ # Privacy Policy for COSE - 多平台文章同步 **Last Updated: December 13, 2025** ## Overview COSE (Create Once, Sync Everywhere) is a browser extension that helps users sync articles from the md.doocs.org Markdown editor to multiple publishing platforms. We are committed to protecting your privacy. ## Data Collection **We do not collect any personal data.** COSE operates entirely locally within your browser. The extension: - Does **NOT** collect personally identifiable information - Does **NOT** collect health, financial, or authentication information - Does **NOT** track your browsing history or web activity - Does **NOT** send any data to external servers - Does **NOT** use analytics or tracking services ## Data Usage All data processed by COSE remains on your local device: - **Article Content**: Your article title, body, and formatting are only read from md.doocs.org and transferred directly to the target publishing platforms within your browser. - **Login Status**: The extension checks login cookies on target platforms (CSDN, Juejin, WeChat, etc.) solely to verify if you are logged in. This information is not stored or transmitted. - **User Preferences**: COSE does not persist user preferences or settings. ## Permissions Explained | Permission | Purpose | |------------|---------| | `tabGroups` | Organize sync tabs into groups | | `activeTab` | Temporarily access the current tab when you initiate a sync | | `scripting` | Fill article content into platform editors | | `cookies` | Check platform login status | | `debugger` | Simulate paste events for WeChat editor | | `clipboardRead` | Read formatted content (HTML) from the clipboard for syncing | | `clipboardWrite` | Write content to the clipboard when needed for syncing | ## Third-Party Services COSE interacts with the following third-party publishing platforms only when you explicitly initiate a sync: - CSDN (csdn.net) - Juejin (juejin.cn) - WeChat Official Account (mp.weixin.qq.com) - And other supported platforms These interactions are solely for the purpose of publishing your content. We have no control over the privacy practices of these platforms. ## Data Security Since no data is collected or transmitted to our servers, there is no risk of data breach from our end. All operations occur locally in your browser. ## Children's Privacy COSE is not directed at children under 13 years of age, and we do not knowingly collect information from children. ## Changes to This Policy We may update this Privacy Policy from time to time. Any changes will be posted on this page with an updated revision date. ## Contact If you have questions about this Privacy Policy, please open an issue at: https://github.com/doocs/cose/issues ## Open Source COSE is open source. You can review the complete source code at: https://github.com/doocs/cose ================================================ FILE: README.md ================================================
$1') // 处理水平分割线 // 注意: X Articles 忽略
---
') html = html.replace(/^\*\*\*$/gm, '***
') // 处理图片 html = html.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, ',转换为 blockquote
// 参考 x-article-publisher skill 的实现
codeBlocks.forEach((block, index) => {
const escapedCode = block.code
.replace(/&/g, '&')
.replace(//g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''')
// 将代码行用
连接,包装在 blockquote 中
// X Articles 原生支持 blockquote,这是最可靠的代码块显示方式
const lines = escapedCode.split('\n').filter(line => line.trim())
const formattedCode = lines.join('
')
const langPrefix = block.lang ? `${block.lang}
` : ''
const codeHtml = `${langPrefix}${formattedCode}
`
html = html.replace(`__CODE_BLOCK_${index}__`, codeHtml)
})
// 恢复行内代码 - X Articles 对 inline style 支持有限
// 使用简单的 标签,依赖平台默认样式
inlineCodes.forEach((code, index) => {
const escapedCode = code
.replace(/&/g, '&')
.replace(//g, '>')
// 简化为纯 code 标签,X Articles 会应用默认样式
const codeHtml = `${escapedCode}`
html = html.replace(`__INLINE_CODE_${index}__`, codeHtml)
})
// 恢复块级公式(使用 CodeCogs API 渲染为图片)
blockFormulas.forEach((formula, index) => {
const encodedFormula = encodeURIComponent(formula)
const formulaHtml = `
`
html = html.replace(`__BLOCK_FORMULA_${index}__`, formulaHtml)
})
// 恢复行内公式
inlineFormulas.forEach((formula, index) => {
const encodedFormula = encodeURIComponent(formula)
const formulaHtml = `
`
html = html.replace(`__INLINE_FORMULA_${index}__`, formulaHtml)
})
// ========== 第四阶段:处理列表和段落 ==========
// 处理无序列表项
html = html.replace(/^[\*\-\+] (.+)$/gm, '$1 ')
// 处理有序列表项
html = html.replace(/^\d+[\.\)] (.+)$/gm, '$1 ')
// 将连续的 包装成
html = html.replace(/(- [\s\S]*?<\/li>\n?)+/g, (match) => {
return `
${match}
`
})
// 处理段落
const lines = html.split('\n')
const result = []
let paragraphLines = []
const isBlockElement = (line) => {
const trimmed = line.trim()
return !trimmed ||
trimmed.startsWith(' {
if (paragraphLines.length > 0) {
result.push(`${paragraphLines.join('
')}
`)
paragraphLines = []
}
}
for (const line of lines) {
const trimmed = line.trim()
if (!trimmed) {
flushParagraph()
continue
}
if (isBlockElement(line)) {
flushParagraph()
result.push(trimmed)
} else {
paragraphLines.push(trimmed)
}
}
flushParagraph()
return result.join('\n')
}
// ========== 主流程 ==========
try {
console.log('[COSE] Twitter Articles 开始填充内容...')
// 使用内置解析器转换 Markdown 为 HTML
const htmlContent = parseMarkdownToHtml(markdown)
console.log('[COSE] Markdown 已转换为 HTML')
// 第一步:填充标题
const titleInput = await waitForElement('textarea[placeholder="Add a title"], textarea[name="Article Title"]', 5000)
if (titleInput && title) {
titleInput.focus()
const nativeSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value').set
nativeSetter.call(titleInput, title)
titleInput.dispatchEvent(new Event('input', { bubbles: true }))
titleInput.dispatchEvent(new Event('change', { bubbles: true }))
console.log('[COSE] Twitter Articles 标题填充成功')
} else {
console.log('[COSE] Twitter Articles 未找到标题输入框')
}
await sleep(500)
// 第二步:填充内容
const contentEl = await waitForElement('.public-DraftEditor-content[contenteditable="true"], .DraftEditor-root [contenteditable="true"]', 5000)
if (contentEl && htmlContent) {
contentEl.focus()
const dt = new DataTransfer()
dt.setData('text/html', htmlContent)
dt.setData('text/plain', htmlContent.replace(/<[^>]*>/g, ''))
const pasteEvent = new ClipboardEvent('paste', {
bubbles: true,
cancelable: true,
clipboardData: dt
})
contentEl.dispatchEvent(pasteEvent)
console.log('[COSE] Twitter Articles 内容填充成功')
return { success: true, method: 'paste-html', length: htmlContent.length }
} else {
console.log('[COSE] Twitter Articles 未找到内容编辑器')
return { success: false, error: 'Content editor not found' }
}
} catch (e) {
console.error('[COSE] Twitter Articles 同步失败:', e)
return { success: false, error: e.message }
}
},
args: [content.title, markdownContent],
world: 'MAIN',
})
console.log('[COSE] Twitter Articles 填充结果:', fillResult[0]?.result)
// 等待内容注入完成
await new Promise(resolve => setTimeout(resolve, 1000))
return { success: true, message: '已同步到 Twitter Articles', tabId: tab.id }
}
// 百度千帆开发者社区:注入 Markdown 并确认转换
// 注意:千帆编辑器有自动保存机制,会触发 POST /api/community/topic
// 该 API 被 OpenRASP WAF 拦截,返回"校验错误,可能是跨站点攻击",导致前端跳转登录页
// 解决方案:
// 1. 使用 declarativeNetRequest 阻止千帆 tab 导航到登录页(网络层拦截)
// 2. 内容脚本 qianfan-intercept.js 拦截 fetch/XHR/sendBeacon/location 跳转(JS 层拦截)
// 3. 监听 tab 的 URL 变化,如果跳转到登录页则导航回编辑器
if (platformId === 'qianfan') {
// 添加 declarativeNetRequest 规则:阻止千帆 tab 导航到登录页
const QIANFAN_BLOCK_RULE_ID = 9999
try {
await chrome.declarativeNetRequest.updateDynamicRules({
removeRuleIds: [QIANFAN_BLOCK_RULE_ID],
addRules: [{
id: QIANFAN_BLOCK_RULE_ID,
priority: 1000,
action: { type: 'block' },
condition: {
urlFilter: '*login.bce.baidu.com*',
initiatorDomains: ['qianfan.cloud.baidu.com'],
resourceTypes: ['main_frame', 'sub_frame']
}
}]
})
console.log('[COSE] 千帆登录页阻止规则已添加')
} catch (e) {
console.warn('[COSE] 千帆登录页阻止规则添加失败:', e)
}
// 打开发布页面
tab = await chrome.tabs.create({ url: platform.publishUrl, active: false })
await addTabToSyncGroup(tab.id, tab.windowId)
// 动态注入千帆拦截脚本(MAIN world,尽早执行)
try {
await chrome.scripting.executeScript({
target: { tabId: tab.id },
func: qianfanIntercept,
world: 'MAIN',
injectImmediately: true,
})
console.log('[COSE] 千帆拦截脚本已动态注入')
} catch (e) {
console.warn('[COSE] 千帆拦截脚本注入失败:', e)
}
// 监听 tab URL 变化,如果跳转到登录页则导航回编辑器
const tabUpdateListener = (tabId, changeInfo) => {
if (tabId === tab.id && changeInfo.url && changeInfo.url.includes('login.bce.baidu.com')) {
console.log('[COSE] 检测到千帆 tab 跳转到登录页,导航回编辑器')
chrome.tabs.update(tabId, { url: platform.publishUrl })
}
}
chrome.tabs.onUpdated.addListener(tabUpdateListener)
try {
// 等待页面加载
await waitForTab(tab.id)
await new Promise(resolve => setTimeout(resolve, 2000))
const markdownContent = content.markdown || content.body || ''
console.log('[COSE] 百度千帆 Markdown 内容长度:', markdownContent?.length || 0)
// 填充标题和内容
const fillResult = await chrome.scripting.executeScript({
target: { tabId: tab.id },
func: async (title, markdown) => {
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms))
const waitForElement = (selector, timeout = 5000) => {
return new Promise((resolve) => {
const el = document.querySelector(selector)
if (el) return resolve(el)
const observer = new MutationObserver(() => {
const el = document.querySelector(selector)
if (el) { observer.disconnect(); resolve(el) }
})
observer.observe(document.body, { childList: true, subtree: true })
setTimeout(() => { observer.disconnect(); resolve(null) }, timeout)
})
}
try {
// 填充标题
const titleInput = await waitForElement('textarea[placeholder="请输入文章标题"]')
if (titleInput && title) {
titleInput.focus()
const nativeSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value').set
nativeSetter.call(titleInput, title)
titleInput.dispatchEvent(new Event('input', { bubbles: true }))
console.log('[COSE] 百度千帆标题填充成功')
}
await sleep(300)
// 填充内容 - 使用 paste 事件注入 Markdown
const contentEditor = await waitForElement('.mp-editor-container[contenteditable="true"]')
if (contentEditor && markdown) {
contentEditor.focus()
await sleep(100)
const dt = new DataTransfer()
dt.setData('text/plain', markdown)
const pasteEvent = new ClipboardEvent('paste', {
bubbles: true, cancelable: true, clipboardData: dt
})
contentEditor.dispatchEvent(pasteEvent)
console.log('[COSE] 百度千帆内容填充成功')
// 等待并点击 Markdown 转换确认按钮
let confirmed = false
for (let i = 0; i < 15; i++) {
await sleep(200)
if (document.body.innerText.includes('检测到 Markdown')) {
const confirmBtn = document.querySelector('.mp-modal-enter-btn')
if (confirmBtn) {
confirmBtn.click()
confirmed = true
console.log('[COSE] 百度千帆已确认 Markdown 转换')
break
}
}
}
await sleep(1000)
return { success: true, confirmed }
}
return { success: false, error: 'Editor not found' }
} catch (e) {
console.error('[COSE] 百度千帆同步失败:', e)
return { success: false, error: e.message }
}
},
args: [content.title, markdownContent],
world: 'MAIN',
})
console.log('[COSE] 百度千帆填充结果:', fillResult[0]?.result)
// 等待内容稳定
await new Promise(resolve => setTimeout(resolve, 2000))
// 清理:移除 tab 监听器和 declarativeNetRequest 规则
chrome.tabs.onUpdated.removeListener(tabUpdateListener)
try {
await chrome.declarativeNetRequest.updateDynamicRules({
removeRuleIds: [QIANFAN_BLOCK_RULE_ID]
})
console.log('[COSE] 千帆登录页阻止规则已移除')
} catch (_) {}
return { success: true, message: '已同步到百度云千帆,请手动点击发布', tabId: tab.id }
} catch (e) {
console.error('[COSE] 千帆同步失败:', e)
chrome.tabs.onUpdated.removeListener(tabUpdateListener)
try {
await chrome.declarativeNetRequest.updateDynamicRules({
removeRuleIds: [QIANFAN_BLOCK_RULE_ID]
})
} catch (_) {}
return { success: false, message: '千帆同步失败: ' + e.message }
}
}
/* [DISABLED] 支付宝开放平台:使用 ne-engine 富文本编辑器,支持 Markdown 转换
if (platformId === 'alipayopen') {
// 先打开发布页面
tab = await chrome.tabs.create({ url: platform.publishUrl, active: false })
await addTabToSyncGroup(tab.id, tab.windowId)
await waitForTab(tab.id)
// 等待页面加载
await new Promise(resolve => setTimeout(resolve, 2000))
const markdownContent = content.markdown || content.body || ''
console.log('[COSE] 支付宝开放平台 Markdown 内容长度:', markdownContent?.length || 0)
// 使用导入的填充函数
const fillResult = await chrome.scripting.executeScript({
target: { tabId: tab.id },
func: fillAlipayOpenContent,
args: [content.title, markdownContent],
world: 'MAIN',
})
console.log('[COSE] 支付宝开放平台填充结果:', fillResult[0]?.result)
// 等待内容处理完成
await new Promise(resolve => setTimeout(resolve, 2000))
return { success: true, message: '已同步到支付宝开放平台', tabId: tab.id }
} else [DISABLED] */
if (platformId !== 'wechat' && !tab) {
// 其他平台(排除微信,因为微信在上面已经处理)
let targetUrl = platform.publishUrl
// 开源中国:使用 ai-write 编辑器,需要用户 ID
if (platformId === 'oschina') {
const stored = await chrome.storage.local.get('oschina_userId')
const userId = stored?.oschina_userId
if (userId) {
targetUrl = `https://my.oschina.net/u/${userId}/blog/ai-write`
console.log('[COSE] 使用 OSChina AI 写作 URL:', targetUrl)
} else {
console.warn('[COSE] 未找到 OSChina 用户 ID,使用默认 URL')
}
}
// 直接打开发布页面
tab = await chrome.tabs.create({ url: targetUrl, active: false })
await addTabToSyncGroup(tab.id, tab.windowId)
await waitForTab(tab.id)
}
// 微信公众号:直接注入 HTML 到编辑器
if (platformId === 'wechat') {
// 使用剪贴板 HTML(带完整样式)或降级到 body
const htmlContent = content.wechatHtml || content.body
console.log('[COSE] 微信 HTML 内容长度:', htmlContent?.length || 0)
// 等待额外时间确保编辑器完全加载
await new Promise(resolve => setTimeout(resolve, 2000))
// 等待编辑器就绪并注入内容
console.log('[COSE] 开始注入微信内容...')
console.log('[COSE] 目标 tab ID:', tab.id)
let result
try {
result = await chrome.scripting.executeScript({
target: { tabId: tab.id },
func: async (title, htmlBody) => {
// 等待元素出现的工具函数
const waitForElement = (selector, timeout = 15000) => {
return new Promise((resolve) => {
const el = document.querySelector(selector)
if (el) return resolve(el)
const observer = new MutationObserver(() => {
const el = document.querySelector(selector)
if (el) {
observer.disconnect()
resolve(el)
}
})
observer.observe(document.body, { childList: true, subtree: true })
setTimeout(() => {
observer.disconnect()
resolve(document.querySelector(selector))
}, timeout)
})
}
try {
// 等待编辑器加载完成
const editor = await waitForElement('.ProseMirror')
if (!editor) {
return { success: false, error: '未找到编辑器' }
}
// 等待标题输入框
const titleInput = await waitForElement('#title')
// 填充标题
if (titleInput && title) {
titleInput.focus()
// 使用 native setter 确保 React/Vue 等框架能检测到变化
const nativeSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value')?.set
if (nativeSetter) {
nativeSetter.call(titleInput, title)
} else {
titleInput.value = title
}
titleInput.dispatchEvent(new Event('input', { bubbles: true }))
titleInput.dispatchEvent(new Event('change', { bubbles: true }))
console.log('[COSE] 微信标题已填充:', title)
}
// 稍等一下让标题生效
await new Promise(r => setTimeout(r, 300))
// 填充正文内容
if (editor && htmlBody) {
editor.focus()
// 清空现有占位符内容
if (editor.textContent.includes('从这里开始写正文')) {
editor.innerHTML = ''
}
// 使用 ClipboardEvent + DataTransfer 注入 HTML
const dt = new DataTransfer()
dt.setData('text/html', htmlBody)
dt.setData('text/plain', htmlBody.replace(/<[^>]*>/g, ''))
const pasteEvent = new ClipboardEvent('paste', {
bubbles: true,
cancelable: true,
clipboardData: dt
})
editor.dispatchEvent(pasteEvent)
console.log('[COSE] 微信内容已通过 paste 事件注入')
// 等待内容渲染
await new Promise(r => setTimeout(r, 500))
// 验证内容是否注入成功
const wordCount = editor.textContent?.length || 0
if (wordCount === 0) {
// 备用方案:直接设置 innerHTML
console.log('[COSE] paste 事件未生效,尝试备用方案')
editor.innerHTML = htmlBody
editor.dispatchEvent(new Event('input', { bubbles: true }))
}
return {
success: true,
wordCount: editor.textContent?.length || 0,
titleFilled: titleInput?.value === title
}
}
return { success: false, error: '内容为空' }
} catch (err) {
return { success: false, error: err.message }
}
},
args: [content.title, htmlContent],
world: 'MAIN',
})
} catch (e) {
console.error('[COSE] executeScript 执行失败:', e)
return { success: false, message: '脚本执行失败: ' + e.message, tabId: tab.id }
}
console.log('[COSE] executeScript 返回数组长度:', result?.length)
console.log('[COSE] executeScript 完整返回:', JSON.stringify(result, null, 2))
if (!result || result.length === 0) {
console.error('[COSE] executeScript 返回空数组')
return { success: false, message: '脚本执行失败:无返回值', tabId: tab.id }
}
const fillResult = result[0].result
console.log('[COSE] 微信填充结果:', JSON.stringify(fillResult, null, 2))
// 检查 result 结构
if (!result || !result[0]) {
console.error('[COSE] executeScript 没有返回有效结果')
return { success: false, message: '内容注入失败:脚本执行无返回值', tabId: tab.id }
}
if (!fillResult?.success) {
console.error('[COSE] 微信内容填充失败:', fillResult?.error)
console.error('[COSE] 完整 result 对象:', result)
return { success: false, message: fillResult?.error || '内容填充失败', tabId: tab.id }
}
console.log('[COSE] 微信内容填充成功,字数:', fillResult.wordCount)
// 等待内容稳定后,点击保存为草稿按钮
await new Promise(resolve => setTimeout(resolve, 1000))
await chrome.scripting.executeScript({
target: { tabId: tab.id },
func: () => {
const saveDraftBtn = Array.from(document.querySelectorAll('button'))
.find(b => b.textContent.includes('保存为草稿'))
if (saveDraftBtn) {
saveDraftBtn.click()
console.log('[COSE] 已点击保存为草稿')
}
},
world: 'MAIN',
})
return { success: true, message: '已同步并保存为草稿', tabId: tab.id }
}
// 抖音:使用剪贴板 HTML 粘贴到编辑器(类似微信公众号)
if (platformId === 'douyin') {
// 使用剪贴板 HTML(带完整样式)或降级到 body
const htmlContent = content.wechatHtml || content.body
console.log('[COSE] 抖音 HTML 内容长度:', htmlContent?.length || 0)
console.log('[COSE] 开始注入抖音内容...')
let result
try {
result = await chrome.scripting.executeScript({
target: { tabId: tab.id },
func: async (title, htmlBody) => {
// 等待元素出现的工具函数(检测到立即返回)
const waitForElement = (selector, timeout = 10000) => {
return new Promise((resolve) => {
const el = document.querySelector(selector)
if (el) return resolve(el)
const observer = new MutationObserver(() => {
const el = document.querySelector(selector)
if (el) {
observer.disconnect()
resolve(el)
}
})
observer.observe(document.body, { childList: true, subtree: true })
setTimeout(() => {
observer.disconnect()
resolve(document.querySelector(selector))
}, timeout)
})
}
try {
// 等待编辑器加载完成 - 抖音使用 contenteditable div
const editor = await waitForElement('[contenteditable="true"]')
if (!editor) {
return { success: false, error: '未找到编辑器' }
}
// 等待标题输入框
const titleInput = await waitForElement('input[placeholder*="标题"]')
// 填充标题
if (titleInput && title) {
titleInput.focus()
// 使用 native setter 确保 React 等框架能检测到变化
const nativeSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value')?.set
if (nativeSetter) {
nativeSetter.call(titleInput, title)
} else {
titleInput.value = title
}
titleInput.dispatchEvent(new Event('input', { bubbles: true }))
titleInput.dispatchEvent(new Event('change', { bubbles: true }))
console.log('[COSE] 抖音标题已填充:', title)
}
// 填充正文内容
if (editor && htmlBody) {
editor.focus()
// 清空现有内容
editor.innerHTML = ''
// 使用 ClipboardEvent + DataTransfer 注入 HTML
const dt = new DataTransfer()
dt.setData('text/html', htmlBody)
dt.setData('text/plain', htmlBody.replace(/<[^>]*>/g, ''))
const pasteEvent = new ClipboardEvent('paste', {
bubbles: true,
cancelable: true,
clipboardData: dt
})
editor.dispatchEvent(pasteEvent)
console.log('[COSE] 抖音内容已通过 paste 事件注入')
// 立即验证内容是否注入成功
const wordCount = editor.textContent?.length || 0
if (wordCount === 0) {
// 备用方案:直接设置 innerHTML
console.log('[COSE] paste 事件未生效,尝试备用方案')
editor.innerHTML = htmlBody
editor.dispatchEvent(new Event('input', { bubbles: true }))
}
return {
success: true,
wordCount: editor.textContent?.length || 0,
titleFilled: titleInput?.value === title
}
}
return { success: false, error: '内容为空' }
} catch (err) {
return { success: false, error: err.message }
}
},
args: [content.title, htmlContent],
world: 'MAIN',
})
} catch (e) {
console.error('[COSE] executeScript 执行失败:', e)
return { success: false, message: '脚本执行失败: ' + e.message, tabId: tab.id }
}
console.log('[COSE] 抖音填充结果:', JSON.stringify(result, null, 2))
if (!result || result.length === 0) {
return { success: false, message: '脚本执行失败:无返回值', tabId: tab.id }
}
const fillResult = result[0].result
if (!fillResult?.success) {
return { success: false, message: fillResult?.error || '内容填充失败', tabId: tab.id }
}
console.log('[COSE] 抖音内容填充成功,字数:', fillResult.wordCount)
return { success: true, message: '已同步到抖音', tabId: tab.id }
}
// 搜狐号:使用剪贴板 HTML 粘贴到编辑器(类似微信公众号)
if (platformId === 'sohu') {
// 等待页面完全加载
await new Promise(resolve => setTimeout(resolve, 3000))
// 使用剪贴板 HTML(带完整样式)或降级到 body
const htmlContent = content.wechatHtml || content.body
console.log('[COSE] 搜狐号 HTML 内容长度:', htmlContent?.length || 0)
// 填充标题和内容
await chrome.scripting.executeScript({
target: { tabId: tab.id },
func: (title, htmlBody) => {
// 填充标题
const titleInput = document.querySelector('input[placeholder*="标题"]')
if (titleInput && title) {
titleInput.focus()
const nativeSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set
nativeSetter.call(titleInput, title)
titleInput.dispatchEvent(new Event('input', { bubbles: true }))
titleInput.dispatchEvent(new Event('change', { bubbles: true }))
console.log('[COSE] 搜狐号标题填充成功')
}
// 找到 Quill 编辑器
const editor = document.querySelector('.ql-editor')
if (editor && htmlBody) {
editor.focus()
// 清空现有内容
editor.innerHTML = ''
// 使用 DataTransfer 触发 paste 事件
const dt = new DataTransfer()
dt.setData('text/html', htmlBody)
dt.setData('text/plain', htmlBody.replace(/<[^>]*>/g, ''))
const pasteEvent = new ClipboardEvent('paste', {
bubbles: true,
cancelable: true,
clipboardData: dt
})
editor.dispatchEvent(pasteEvent)
console.log('[COSE] 搜狐号内容已通过 paste 事件注入')
} else {
console.log('[COSE] 搜狐号未找到编辑器')
}
},
args: [content.title, htmlContent],
world: 'MAIN',
})
// 等待内容注入完成
await new Promise(resolve => setTimeout(resolve, 2000))
return { success: true, message: '已同步到搜狐号', tabId: tab.id }
}
// B站专栏:使用 UEditor execCommand 插入 HTML
if (platformId === 'bilibili') {
// 使用剪贴板 HTML(带完整样式)或降级到 body
const htmlContent = content.wechatHtml || content.body
console.log('[COSE] B站专栏 HTML 内容长度:', htmlContent?.length || 0)
// 等待 UEditor 就绪
const waitForEditor = await chrome.scripting.executeScript({
target: { tabId: tab.id },
func: () => {
return new Promise((resolve) => {
const startTime = Date.now()
const maxWait = 10000
const check = () => {
const UE = window.UE
if (UE && UE.instants && UE.instants['ueditorInstant0']) {
const editor = UE.instants['ueditorInstant0']
if (editor.isReady) {
console.log('[COSE] UEditor 已就绪,耗时:', Date.now() - startTime, 'ms')
resolve({ ready: true, time: Date.now() - startTime })
return
}
}
if (Date.now() - startTime > maxWait) {
console.log('[COSE] UEditor 等待超时')
resolve({ ready: false, timeout: true })
return
}
setTimeout(check, 100)
}
check()
})
},
world: 'MAIN',
})
console.log('[COSE] B站专栏编辑器状态:', waitForEditor)
// 填充标题和内容(一次性完成)
const fillResult = await chrome.scripting.executeScript({
target: { tabId: tab.id },
func: (title, htmlBody) => {
// 填充标题
const titleInput = document.querySelector('textarea')
if (titleInput && title) {
titleInput.focus()
titleInput.value = title
titleInput.dispatchEvent(new Event('input', { bubbles: true }))
titleInput.dispatchEvent(new Event('change', { bubbles: true }))
console.log('[COSE] B站专栏标题填充成功')
}
// 填充内容
const UE = window.UE
if (!UE || !UE.instants) {
return { success: false, error: 'UEditor not found' }
}
const editor = UE.instants['ueditorInstant0']
if (!editor) {
return { success: false, error: 'UEditor instance not found' }
}
// 清空并插入内容
editor.setContent('')
editor.execCommand('inserthtml', htmlBody)
editor.fireEvent('contentchange')
console.log('[COSE] B站专栏内容已填充')
return {
success: true,
contentLength: editor.getContentLength()
}
},
args: [content.title, htmlContent],
world: 'MAIN',
})
console.log('[COSE] B站专栏填充结果:', fillResult)
// 短暂等待后点击存草稿
await new Promise(resolve => setTimeout(resolve, 300))
// 点击存草稿按钮
await chrome.scripting.executeScript({
target: { tabId: tab.id },
func: () => {
const saveDraftBtn = Array.from(document.querySelectorAll('button'))
.find(b => b.textContent && b.textContent.includes('存草稿'))
if (saveDraftBtn) {
saveDraftBtn.click()
console.log('[COSE] B站专栏已点击存草稿')
}
},
world: 'MAIN',
})
// 等待保存完成
await new Promise(resolve => setTimeout(resolve, 500))
return { success: true, message: '已同步并保存草稿到B站专栏', tabId: tab.id }
}
// 微博头条:使用 ProseMirror 编辑器
if (platformId === 'weibo') {
// 等待页面完全加载
await new Promise(resolve => setTimeout(resolve, 3000))
// 使用剪贴板 HTML(带完整样式)或降级到 body
const htmlContent = content.wechatHtml || content.body
console.log('[COSE] 微博头条 HTML 内容长度:', htmlContent?.length || 0)
// 填充标题和内容
const fillResult = await chrome.scripting.executeScript({
target: { tabId: tab.id },
func: (title, htmlBody) => {
// 填充标题
const titleInput = document.querySelector('textarea[placeholder*="标题"]')
if (titleInput && title) {
titleInput.focus()
titleInput.value = title
titleInput.dispatchEvent(new Event('input', { bubbles: true }))
titleInput.dispatchEvent(new Event('change', { bubbles: true }))
console.log('[COSE] 微博头条标题填充成功')
}
// 填充内容 - 微博使用 ProseMirror/TipTap 编辑器
const editor = document.querySelector('.ProseMirror')
if (editor && htmlBody) {
editor.innerHTML = htmlBody
editor.dispatchEvent(new Event('input', { bubbles: true }))
console.log('[COSE] 微博头条内容填充成功')
return { success: true }
}
return { success: false, error: 'Editor not found' }
},
args: [content.title, htmlContent],
world: 'MAIN',
})
console.log('[COSE] 微博头条填充结果:', fillResult)
// 等待内容注入完成
await new Promise(resolve => setTimeout(resolve, 1000))
// 点击保存草稿按钮
await chrome.scripting.executeScript({
target: { tabId: tab.id },
func: () => {
const saveBtn = Array.from(document.querySelectorAll('button'))
.find(b => b.textContent && b.textContent.includes('保存草稿'))
if (saveBtn) {
saveBtn.click()
console.log('[COSE] 微博头条已点击保存草稿')
}
},
world: 'MAIN',
})
// 等待保存完成
await new Promise(resolve => setTimeout(resolve, 1000))
return { success: true, message: '已同步到微博头条', tabId: tab.id }
}
// 阿里云开发者社区:使用 Markdown 编辑器
if (platformId === 'aliyun') {
// 等待页面完全加载
await new Promise(resolve => setTimeout(resolve, 3000))
// 阿里云使用 Markdown 编辑器
const markdownContent = content.markdown || content.body || ''
console.log('[COSE] 阿里云开发者社区 Markdown 内容长度:', markdownContent?.length || 0)
// 填充标题和内容
const fillResult = await chrome.scripting.executeScript({
target: { tabId: tab.id },
func: (title, markdown) => {
// 填充标题
const titleInput = document.querySelector('input[placeholder*="标题"]')
if (titleInput && title) {
titleInput.focus()
// 使用 native setter 来绕过 React 的受控组件
const nativeSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set
nativeSetter.call(titleInput, title)
titleInput.dispatchEvent(new Event('input', { bubbles: true }))
titleInput.dispatchEvent(new Event('change', { bubbles: true }))
console.log('[COSE] 阿里云开发者社区标题填充成功')
}
// 填充内容 - 阿里云使用 textarea 作为 Markdown 编辑器
const contentTextarea = document.querySelector('textarea[class*="editor"]') ||
document.querySelector('.markdown-editor textarea') ||
document.querySelector('textarea:not([placeholder*="标题"])')
if (contentTextarea && markdown) {
contentTextarea.focus()
// 使用 native setter 来绕过 React 的受控组件
const nativeSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value').set
nativeSetter.call(contentTextarea, markdown)
contentTextarea.dispatchEvent(new Event('input', { bubbles: true }))
contentTextarea.dispatchEvent(new Event('change', { bubbles: true }))
console.log('[COSE] 阿里云开发者社区内容填充成功')
return { success: true }
}
return { success: false, error: 'Editor not found' }
},
args: [content.title, markdownContent],
world: 'MAIN',
})
console.log('[COSE] 阿里云开发者社区填充结果:', fillResult)
// 等待内容注入完成
await new Promise(resolve => setTimeout(resolve, 1000))
return { success: true, message: '已同步到阿里云开发者社区', tabId: tab.id }
}
// 火山引擎开发者社区:使用 ByteMD 编辑器(基于 CodeMirror)
if (platformId === 'volcengine') {
// 等待页面完全加载
await new Promise(resolve => setTimeout(resolve, 3000))
// 火山引擎使用 Markdown 编辑器
const markdownContent = content.markdown || content.body || ''
console.log('[COSE] 火山引擎开发者社区 Markdown 内容长度:', markdownContent?.length || 0)
// 填充标题和内容
const fillResult = await chrome.scripting.executeScript({
target: { tabId: tab.id },
func: (title, markdown) => {
// 填充标题
const titleInput = document.querySelector('input[placeholder*="标题"]') ||
document.querySelector('input[class*="title"]') ||
document.querySelector('.article-title input')
if (titleInput && title) {
titleInput.focus()
// 使用 native setter 来绕过 React 的受控组件
const nativeSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set
nativeSetter.call(titleInput, title)
titleInput.dispatchEvent(new Event('input', { bubbles: true }))
titleInput.dispatchEvent(new Event('change', { bubbles: true }))
console.log('[COSE] 火山引擎开发者社区标题填充成功')
}
// 火山引擎使用 ByteMD 编辑器(基于 CodeMirror)
const codeMirrorEl = document.querySelector('.CodeMirror')
if (codeMirrorEl && codeMirrorEl.CodeMirror && markdown) {
codeMirrorEl.CodeMirror.setValue(markdown)
console.log('[COSE] 火山引擎开发者社区内容填充成功')
return { success: true, method: 'CodeMirror' }
}
// 备用方案:尝试直接操作 textarea
const contentTextarea = document.querySelector('.bytemd-editor textarea') ||
document.querySelector('textarea:not([placeholder*="标题"])')
if (contentTextarea && markdown) {
contentTextarea.focus()
const nativeSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value').set
nativeSetter.call(contentTextarea, markdown)
contentTextarea.dispatchEvent(new Event('input', { bubbles: true }))
contentTextarea.dispatchEvent(new Event('change', { bubbles: true }))
console.log('[COSE] 火山引擎开发者社区内容填充成功(textarea)')
return { success: true, method: 'textarea' }
}
return { success: false, error: 'Editor not found' }
},
args: [content.title, markdownContent],
world: 'MAIN',
})
console.log('[COSE] 火山引擎开发者社区填充结果:', fillResult)
// 等待内容注入完成
await new Promise(resolve => setTimeout(resolve, 1000))
return { success: true, message: '已同步到火山引擎开发者社区', tabId: tab.id }
}
// 华为云开发者博客:使用 Markdown 编辑器(在 iframe 中)
if (platformId === 'huaweicloud') {
// 等待页面完全加载
await new Promise(resolve => setTimeout(resolve, 3000))
// 华为云使用 Markdown 编辑器
const markdownContent = content.markdown || content.body || ''
console.log('[COSE] 华为云开发者博客 Markdown 内容长度:', markdownContent?.length || 0)
// 检查当前编辑器类型,如果不是 Markdown 则切换
const switchResult = await chrome.scripting.executeScript({
target: { tabId: tab.id },
func: () => {
if (window.tinymceModal?.currentEditorType === 'markdown') {
console.log('[COSE] 华为云已经是 Markdown 编辑器')
return { alreadyMarkdown: true }
}
const allElements = document.querySelectorAll('*')
for (const el of allElements) {
if (el.textContent === 'Markdown格式编辑' && el.children.length === 0) {
el.click()
console.log('[COSE] 华为云已点击 Markdown 编辑器标签')
return { clicked: true }
}
}
return { clicked: false }
},
world: 'MAIN',
})
// 如果点击了切换按钮,需要等待确认对话框并点击确定
if (switchResult[0]?.result?.clicked) {
await new Promise(resolve => setTimeout(resolve, 500))
await chrome.scripting.executeScript({
target: { tabId: tab.id },
func: () => {
const allElements = document.querySelectorAll('*')
for (const el of allElements) {
if (el.textContent === '确定' && el.children.length === 0) {
el.click()
console.log('[COSE] 华为云已点击确定按钮')
return { confirmed: true }
}
}
return { confirmed: false }
},
world: 'MAIN',
})
// 等待编辑器切换完成
await new Promise(resolve => setTimeout(resolve, 3000))
}
// 填充标题
await chrome.scripting.executeScript({
target: { tabId: tab.id },
func: (title) => {
const titleInput = document.querySelector('input[placeholder*="标题"]')
if (titleInput && title) {
titleInput.focus()
titleInput.value = title
titleInput.dispatchEvent(new Event('input', { bubbles: true }))
titleInput.dispatchEvent(new Event('change', { bubbles: true }))
console.log('[COSE] 华为云开发者博客标题填充成功')
}
},
args: [content.title],
world: 'MAIN',
})
// 等待 Markdown 编辑器 iframe 完全就绪,然后填充内容
// 使用 MutationObserver 监听 iframe 出现,message 事件监听内容确认
const fillResult = await chrome.scripting.executeScript({
target: { tabId: tab.id },
func: async (markdown) => {
// 工具函数:使用 MutationObserver 等待 iframe 元素出现并加载
const waitForEditorReady = (timeout = 15000) => {
return new Promise((resolve) => {
const check = () => {
const editor = window.tinymceModal?.currentEditor
if (editor && typeof editor.setContent === 'function') {
const iframe = document.getElementById(editor.editor_id)
if (iframe && iframe.contentWindow) {
return { editor, iframe }
}
}
return null
}
// 先立即检查一次
const immediate = check()
if (immediate) return resolve(immediate)
// 使用 MutationObserver 监听 DOM 变化(iframe 插入)
let resolved = false
const observer = new MutationObserver(() => {
if (resolved) return
const result = check()
if (result) {
resolved = true
observer.disconnect()
resolve(result)
}
})
observer.observe(document.body, { childList: true, subtree: true })
// 超时兜底
setTimeout(() => {
if (!resolved) {
resolved = true
observer.disconnect()
resolve(null)
}
}, timeout)
})
}
// 工具函数:使用 message 事件监听 setContent 确认(setMdDataSucc)
const setContentWithConfirm = (editor, iframe, content, timeout = 3000) => {
return new Promise((resolve) => {
let resolved = false
const onMessage = (event) => {
try {
const data = typeof event.data === 'string' ? JSON.parse(event.data) : event.data
if (data.mdEventAction === 'setMdDataSucc' || data.mdEventAction === 'mdContent') {
if (!resolved) {
resolved = true
window.removeEventListener('message', onMessage)
resolve({ confirmed: true })
}
}
} catch (e) { /* 忽略非 JSON 消息 */ }
}
window.addEventListener('message', onMessage)
editor.setContent(content)
// 超时兜底
setTimeout(() => {
if (!resolved) {
resolved = true
window.removeEventListener('message', onMessage)
resolve({ confirmed: false })
}
}, timeout)
})
}
// 1. 等待编辑器和 iframe 就绪
console.log('[COSE] 华为云:等待 Markdown 编辑器 iframe 就绪...')
const ready = await waitForEditorReady()
if (!ready) {
console.log('[COSE] 华为云:编辑器等待超时')
return { success: false, error: '编辑器 iframe 等待超时' }
}
console.log('[COSE] 华为云:编辑器 iframe 已就绪')
// 2. 带重试的内容填充,通过 message 事件确认
const maxRetries = 6
for (let attempt = 1; attempt <= maxRetries; attempt++) {
console.log(`[COSE] 华为云内容填充尝试 ${attempt}/${maxRetries}`)
const result = await setContentWithConfirm(ready.editor, ready.iframe, markdown)
if (result.confirmed) {
console.log(`[COSE] 华为云内容填充成功(第${attempt}次),已收到 iframe 确认`)
return { success: true, method: 'message-confirm', attempt, length: markdown.length }
}
console.log(`[COSE] 华为云:未收到 iframe 确认,等待后重试...`)
// iframe 内部应用可能还在初始化,等待后重试
await new Promise(r => setTimeout(r, 2000))
}
// 3. 所有重试失败,直接 postMessage 作为最后手段
console.log('[COSE] 重试耗尽,尝试直接 postMessage')
ready.iframe.contentWindow.postMessage(JSON.stringify({
mdEditorEventAction: 'setMdEditorContent',
data: encodeURIComponent(markdown)
}), '*')
await new Promise(r => setTimeout(r, 1000))
return { success: true, method: 'direct-postMessage', length: markdown.length }
},
args: [markdownContent],
world: 'MAIN',
})
console.log('[COSE] 华为云开发者博客填充结果:', fillResult)
// 等待内容注入完成
await new Promise(resolve => setTimeout(resolve, 1000))
// 点击保存草稿按钮
await chrome.scripting.executeScript({
target: { tabId: tab.id },
func: () => {
const allLinks = document.querySelectorAll('a')
for (const link of allLinks) {
if (link.textContent && link.textContent.includes('保存草稿')) {
link.click()
console.log('[COSE] 华为云开发者博客已点击保存草稿')
return { clicked: true }
}
}
return { clicked: false }
},
world: 'MAIN',
})
// 等待保存完成
await new Promise(resolve => setTimeout(resolve, 1000))
return { success: true, message: '已同步到华为云开发者博客', tabId: tab.id }
}
// 华为开发者文章:使用 ACE Editor (Markdown 编辑器)
if (platformId === 'huaweidev') {
// 华为开发者文章使用 Markdown 编辑器
const markdownContent = content.markdown || content.body || ''
console.log('[COSE] 华为开发者文章 Markdown 内容长度:', markdownContent?.length || 0)
// 注入异步弹窗监听器,并执行主流程
const result = await chrome.scripting.executeScript({
target: { tabId: tab.id },
func: async (title, markdown) => {
// ========== 弹窗处理函数 ==========
const handleDialog = () => {
// 方法1: 查找 Ant Design Modal 中的按钮
const modalBtns = document.querySelector('.ant-modal-confirm-btns')
if (modalBtns) {
const buttons = modalBtns.querySelectorAll('button')
const modalText = document.querySelector('.ant-modal-confirm-content')?.textContent || ''
console.log('[COSE] 检测到 Ant Modal:', modalText.substring(0, 50))
// 处理"温馨提示"弹窗 - 点击"取消"
if (modalText.includes('温馨提示') || modalText.includes('未保存')) {
for (const btn of buttons) {
if (btn.textContent?.trim() === '取消') {
console.log('[COSE] 点击温馨提示弹窗的取消按钮')
btn.click()
return true
}
}
}
// 处理 MD 编辑器切换确认对话框 - 点击"确认"
if (modalText.includes('Markdown') || modalText.includes('切换')) {
for (const btn of buttons) {
if (btn.textContent?.trim() === '确认') {
console.log('[COSE] 点击 MD 切换确认按钮')
btn.click()
return true
}
}
}
}
// 方法2: 查找 HTML5 dialog 元素(备用)
const dialog = document.querySelector('dialog[open]')
if (dialog) {
const dialogText = dialog.textContent || ''
const buttons = dialog.querySelectorAll('button')
console.log('[COSE] 检测到 dialog:', dialogText.substring(0, 50))
if (dialogText.includes('温馨提示') || dialogText.includes('未保存')) {
for (const btn of buttons) {
if (btn.textContent?.trim() === '取消') {
btn.click()
return true
}
}
}
if (dialogText.includes('Markdown') || dialogText.includes('切换')) {
for (const btn of buttons) {
if (btn.textContent?.trim() === '确认') {
btn.click()
return true
}
}
}
}
return false
}
// ========== 工具函数 ==========
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms))
// 轮询检查弹窗(比 MutationObserver 更可靠)
let dialogCheckInterval = null
const startDialogChecker = () => {
// 先检查已存在的弹窗
handleDialog()
// 定时检查新弹窗
dialogCheckInterval = setInterval(() => {
handleDialog()
}, 200)
console.log('[COSE] 华为开发者文章弹窗检查器已启动')
}
const stopDialogChecker = () => {
if (dialogCheckInterval) {
clearInterval(dialogCheckInterval)
dialogCheckInterval = null
console.log('[COSE] 华为开发者文章弹窗检查器已停止')
}
}
const waitForElement = async (selector, timeout = 5000) => {
const start = Date.now()
while (Date.now() - start < timeout) {
const el = document.querySelector(selector)
if (el) return el
await sleep(100)
}
return null
}
// 等待按钮出现(支持 button 和 a 标签)
const waitForMdButton = async (timeout = 15000) => {
const start = Date.now()
while (Date.now() - start < timeout) {
// 方法1: 通过 CKEditor 的 class 选择器(最精确)
let btn = document.querySelector('a.cke_button__cktomd')
if (btn) return btn
// 方法2: 查找包含 "MD编辑器" 文本的 标签
const allLinks = document.querySelectorAll('a')
for (const link of allLinks) {
if (link.textContent?.trim() === 'MD编辑器') {
return link
}
}
// 方法3: 查找包含 "MD编辑器" 文本的