feat: elecfans platform sync (#136)

* feat: elecfans platform sync

* Adjust Elecfans logo size in README

Updated the Elecfans logo dimensions in README.md.
This commit is contained in:
Neo
2026-01-26 19:29:10 +08:00
committed by GitHub
parent 48793cea19
commit 81ff1cd510
6 changed files with 242 additions and 3 deletions
+3 -2
View File
@@ -47,7 +47,7 @@ _**C**reate **O**nce **S**ync **E**verywhere_
> | A | 阿里云社区 |
> | B | B站专栏、百度云千帆、百家号、博客园 |
> | C | CSDN |
> | D | 抖音文章 |
> | D | 电子发烧友、抖音文章 |
> | H | 华为开发者文章、华为云博客、火山引擎社区 |
> | I | InfoQ |
> | J | 简书、掘金、今日头条 |
@@ -78,7 +78,7 @@ _**C**reate **O**nce **S**ync **E**verywhere_
<img src="https://favicon.im/sohu.com?larger=true" alt="搜狐号" width="60" height="60" />
<img src="https://cdn.simpleicons.org/sinaweibo" alt="微博文章" width="60" height="60" />
<img src="https://cdn.simpleicons.org/bilibili" alt="B站专栏" width="60" height="60" />
<img src="https://icon.horse/icon/sspai.com" alt="少数派" width="60" height="60" />
<img src="https://favicon.im/sspai.com?larger=true" alt="少数派" width="60" height="60" />
<img src="https://cdn.simpleicons.org/x" alt="X(Formerly Twitter) Articles" width="50" height="60" />
</div>
</div>
@@ -120,6 +120,7 @@ _**C**reate **O**nce **S**ync **E**verywhere_
<img src="https://cdn.jsdelivr.net/npm/@lobehub/icons-static-png/light/modelscope-text.png" alt="魔搭社区" width="160" height="60" />
<img src="https://cdn.jsdelivr.net/npm/@lobehub/icons-static-png/dark/volcengine-color.png" alt="火山引擎" width="36" height="60" />
<img src="https://cdn.jsdelivr.net/npm/@lobehub/icons-static-png/light/volcengine-text.png" alt="火山引擎" width="90" height="60" />
<img src="https://www.elecfans.com/static/main/img/elecfans-logo.jpg" alt="电子发烧友" width="35" height="60" />
</div>
</div>
+2 -1
View File
@@ -48,7 +48,8 @@
"https://*.modelscope.cn/*",
"https://*.volcengine.com/*",
"https://*.douyin.com/*",
"https://*.xiaohongshu.com/*"
"https://*.xiaohongshu.com/*",
"https://*.elecfans.com/*"
],
"action": {
"default_icon": {
+207
View File
@@ -390,6 +390,67 @@ async function checkPlatformLogin(platform) {
}
}
// 电子发烧友特殊处理:直接调用 API 检测登录状态
if (platform.id === 'elecfans') {
try {
// 先获取 elecfans 的登录 cookie
const authCookie = await chrome.cookies.get({
url: 'https://www.elecfans.com',
name: 'auth'
})
const authWwwCookie = await chrome.cookies.get({
url: 'https://www.elecfans.com',
name: 'auth_www'
})
console.log(`[COSE] elecfans cookies: auth=${!!authCookie}, auth_www=${!!authWwwCookie}`)
if (!authCookie && !authWwwCookie) {
console.log(`[COSE] elecfans 未找到登录 cookie,未登录`)
return { loggedIn: false }
}
// 直接调用 API 获取用户信息
try {
const response = await fetch('https://www.elecfans.com/webapi/passport/checklogin?_=' + Date.now(), {
method: 'GET',
credentials: 'include',
headers: {
'Accept': 'application/json, text/javascript, */*; q=0.01',
}
})
if (!response.ok) {
console.log(`[COSE] elecfans API 响应错误:`, response.status)
return { loggedIn: true, username: '', avatar: '' }
}
const data = await response.json()
console.log(`[COSE] elecfans API 数据:`, data)
// API 返回格式: {"uid":"6997925","username":"jf_50332692","avatar":"https://..."}
if (data && data.uid) {
const username = data.username || ''
const avatar = data.avatar || ''
console.log(`[COSE] elecfans 用户信息:`, username, avatar ? '有头像' : '无头像')
return { loggedIn: true, username, avatar }
} else {
// 有 cookie 但 API 返回无用户,仍然认为已登录
console.log(`[COSE] elecfans API 返回无用户数据,但有 cookie`)
return { loggedIn: true, username: '', avatar: '' }
}
} catch (e) {
console.log(`[COSE] elecfans API 调用失败:`, e.message)
// API 失败但有 cookie,仍然认为已登录
return { loggedIn: true, username: '', avatar: '' }
}
} catch (e) {
console.log(`[COSE] elecfans 检测失败:`, e.message)
return { loggedIn: false }
}
}
try {
console.log(`[COSE] ${platform.id} 开始 API 检测:`, config.api)
const controller = new AbortController()
@@ -3534,6 +3595,152 @@ async function syncToPlatform(platformId, content) {
return { success: true, message: '已同步到支付宝开放平台', tabId: tab.id }
}
// 电子发烧友:等待 Vditor 编辑器加载后填充内容
if (platformId === 'elecfans') {
// 等待页面完全加载
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: async (title, markdown) => {
// 等待元素出现的工具函数
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 {
console.log('[COSE] 电子发烧友开始填充内容...')
// 等待并查找标题输入框
const titleInput = await waitForElement('input[placeholder*="标题"], input.title-input, input[name="title"]', 5000)
if (titleInput && title) {
titleInput.focus()
// 使用 native setter 确保框架能检测到变化
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)
}
// 稍等一下让标题生效
await new Promise(r => setTimeout(r, 500))
// 优先查找 Vditor 编辑器(电子发烧友使用 Vditor)
const vditorWysiwyg = document.querySelector('.vditor-wysiwyg .vditor-reset')
if (vditorWysiwyg) {
vditorWysiwyg.focus()
// Vditor 需要 HTML 内容,将 Markdown 转换为简单 HTML
// 先尝试使用 ClipboardEvent 粘贴 Markdown
const dt = new DataTransfer()
dt.setData('text/plain', markdown)
const pasteEvent = new ClipboardEvent('paste', {
bubbles: true,
cancelable: true,
clipboardData: dt
})
vditorWysiwyg.dispatchEvent(pasteEvent)
console.log('[COSE] 电子发烧友 Vditor paste 事件已触发')
// 等待内容渲染
await new Promise(r => setTimeout(r, 500))
// 验证内容是否注入成功
const wordCount = vditorWysiwyg.textContent?.length || 0
if (wordCount > 10) {
console.log('[COSE] 电子发烧友 Vditor 内容已填充,字数:', wordCount)
return { success: true, method: 'vditor-paste', length: wordCount }
}
// 备用方案:直接设置 textContentVditor 会自动解析 Markdown
console.log('[COSE] paste 事件未生效,尝试直接输入')
vditorWysiwyg.textContent = markdown
vditorWysiwyg.dispatchEvent(new Event('input', { bubbles: true }))
return { success: true, method: 'vditor-direct', length: markdown.length }
}
// 等待并查找 CodeMirror 编辑器或 textarea
const cmElement = document.querySelector('.CodeMirror')
if (cmElement && cmElement.CodeMirror) {
cmElement.CodeMirror.setValue(markdown)
console.log('[COSE] 电子发烧友 CodeMirror 内容已填充')
return { success: true, method: 'codemirror', length: markdown.length }
}
// 尝试查找 textarea
const textarea = await waitForElement('textarea.content-textarea, textarea[name="content"], textarea', 5000)
if (textarea && markdown) {
textarea.focus()
const nativeSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value')?.set
if (nativeSetter) {
nativeSetter.call(textarea, markdown)
} else {
textarea.value = markdown
}
textarea.dispatchEvent(new Event('input', { bubbles: true }))
textarea.dispatchEvent(new Event('change', { bubbles: true }))
console.log('[COSE] 电子发烧友 textarea 内容已填充')
return { success: true, method: 'textarea', length: markdown.length }
}
// 尝试查找通用 contenteditable 编辑器
const editor = await waitForElement('[contenteditable="true"]', 5000)
if (editor && markdown) {
editor.focus()
editor.textContent = markdown
editor.dispatchEvent(new Event('input', { bubbles: true }))
console.log('[COSE] 电子发烧友 contenteditable 内容已填充')
return { success: true, method: 'contenteditable', length: markdown.length }
}
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, 1000))
return { success: true, message: '已同步到电子发烧友', tabId: tab.id }
}
// 其他平台使用 scripting API 直接注入填充脚本
// 使用 MAIN world 才能访问页面的 CodeMirror 实例
await chrome.scripting.executeScript({
+1
View File
@@ -92,6 +92,7 @@
{ id: 'volcengine', name: 'Volcengine', icon: 'https://lf1-cdn-tos.bytegoofy.com/goofy/tech-fe/fav.png', title: '火山引擎开发者社区', type: 'volcengine', url: 'https://developer.volcengine.com/articles/draft' },
{ id: 'douyin', name: 'Douyin', icon: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/yvahlyj_upfbvk_zlp/ljhwZthlaukjlkulzlp/pc_creator/favicon_v2_7145ff0.ico', title: '抖音文章', type: 'douyin', url: 'https://creator.douyin.com/creator-micro/content/post/article?default-tab=5&enter_from=publish_page&media_type=article&type=new' },
{ id: 'xiaohongshu', name: 'Xiaohongshu', icon: 'https://www.xiaohongshu.com/favicon.ico', title: '小红书', type: 'xiaohongshu', url: 'https://creator.xiaohongshu.com/publish/publish?from=menu&target=article' },
{ id: 'elecfans', name: 'Elecfans', icon: 'https://www.elecfans.com/favicon.ico', title: '电子发烧友', type: 'elecfans', url: 'https://www.elecfans.com/d/article/md/' },
]
// 暴露 $cose 全局对象
+25
View File
@@ -0,0 +1,25 @@
// 电子发烧友平台配置
export const ElecfansPlatform = {
id: 'elecfans',
name: '电子发烧友',
icon: 'https://www.elecfans.com/favicon.ico',
publishUrl: 'https://www.elecfans.com/d/article/md/',
loginUrl: 'https://bbs.elecfans.com/member.php?mod=logging&action=login',
}
export const ElecfansLoginConfig = {
type: 'api',
apiUrl: 'https://www.elecfans.com/webapi/passport/checklogin',
checkLoggedIn: (data) => {
// API 返回格式: {"uid":"6999925","username":"jf_50493572","avatar":"https://..."}
if (data && data.uid) {
return {
loggedIn: true,
username: data.username || '',
avatar: data.avatar || '',
}
}
return { loggedIn: false }
},
}
+4
View File
@@ -28,6 +28,7 @@ import { ModelScopePlatform, ModelScopeLoginConfig } from './modelscope.js'
import { VolcenginePlatform, VolcengineLoginConfig } from './volcengine.js'
import { DouyinPlatform, DouyinLoginConfig } from './douyin.js'
import { XiaohongshuPlatform, XiaohongshuLoginConfig } from './xiaohongshu.js'
import { ElecfansPlatform, ElecfansLoginConfig } from './elecfans.js'
// 合并平台配置
const PLATFORMS = [
@@ -60,6 +61,7 @@ const PLATFORMS = [
VolcenginePlatform,
DouyinPlatform,
XiaohongshuPlatform,
ElecfansPlatform,
]
// 合并登录检测配置
@@ -93,6 +95,7 @@ const LOGIN_CHECK_CONFIG = {
[VolcenginePlatform.id]: VolcengineLoginConfig,
[DouyinPlatform.id]: DouyinLoginConfig,
[XiaohongshuPlatform.id]: XiaohongshuLoginConfig,
[ElecfansPlatform.id]: ElecfansLoginConfig,
}
// 根据 hostname 获取平台填充函数
@@ -126,6 +129,7 @@ function getPlatformFiller(hostname) {
if (hostname.includes('developer.volcengine.com')) return 'volcengine'
if (hostname.includes('creator.douyin.com')) return 'douyin'
if (hostname.includes('creator.xiaohongshu.com')) return 'xiaohongshu'
if (hostname.includes('elecfans.com')) return 'elecfans'
return 'generic'
}