feat: add segmentfault support (#21)

This commit is contained in:
Neo
2025-12-21 17:15:33 +08:00
committed by GitHub
parent 9016263d98
commit 8ee17dcb23
5 changed files with 170 additions and 21 deletions
+8 -9
View File
@@ -35,17 +35,16 @@ _**C**reate **O**nce **S**ync **E**verywhere_
- 微信公众号同步时完整保留渲染样式并自动保存为草稿
## 支持的平台
## 支持的平台
| 平台 | 说明 |
|------|------|
| CSDN | 自动填充 Markdown 内容 |
| 掘金 | 自动填充 Markdown 内容 |
| 微信公众号 | 保留完整样式,自动保存草稿 |
| 知乎 | 自动填充 Markdown 内容 |
| 今日头条 | 自动填充 Markdown 内容 |
- 微信公众号
- 今日头条
- 知乎
- CSDN
- 掘金
- 思否
更多想要添加的平台欢迎提 [Issue](https://github.com/doocs/cose/issues)
更多想要添加的平台欢迎提 [Issue](https://github.com/doocs/cose/issues)
## 开发者模式测试
+68 -1
View File
@@ -5,6 +5,7 @@ const PLATFORMS = [
{ id: 'wechat', name: 'WeChat', icon: 'https://res.wx.qq.com/a/wx_fed/assets/res/NTI4MWU5.ico', url: 'https://mp.weixin.qq.com', publishUrl: 'https://mp.weixin.qq.com/cgi-bin/appmsg?t=media/appmsg_edit_v2&action=edit&isNew=1&type=10' },
{ id: 'zhihu', name: 'Zhihu', icon: 'https://static.zhihu.com/heifetz/favicon.ico', url: 'https://www.zhihu.com', publishUrl: 'https://zhuanlan.zhihu.com/write' },
{ id: 'toutiao', name: 'Toutiao', icon: 'https://sf3-cdn-tos.toutiaostatic.com/obj/eden-cn/uhbfnupkbps/toutiao_favicon.ico', url: 'https://mp.toutiao.com', publishUrl: 'https://mp.toutiao.com/profile_v4/graphic/publish' },
{ id: 'segmentfault', name: 'SegmentFault', icon: 'https://static.segmentfault.com/main_site_next/prod/favicon.ico', url: 'https://segmentfault.com', publishUrl: 'https://segmentfault.com/write' },
]
// 当前同步任务的 Tab Group ID
@@ -100,6 +101,13 @@ const LOGIN_CHECK_CONFIG = {
avatar: response?.data?.media?.https_avatar_url,
}),
},
segmentfault: {
useCookie: true,
cookieUrl: 'https://segmentfault.com',
cookieNames: ['PHPSESSID'],
fetchUserInfoFromPage: true,
userInfoUrl: 'https://segmentfault.com/write',
},
}
// 消息监听
@@ -279,8 +287,32 @@ async function checkLoginByCookie(platformId, config) {
}
console.log(`[COSE] ${platformId} 用户信息:`, username, avatar ? '有头像' : '无头像')
}
// 思否用户信息提取(从 __NEXT_DATA__ 中获取)
// 注意:思否的 PHPSESSID 即使未登录也存在,必须通过用户信息判断登录状态
else if (platformId === 'segmentfault') {
// 从 __NEXT_DATA__ 中提取用户信息
const nextDataMatch = html.match(/<script\s+id="__NEXT_DATA__"[^>]*>([^<]+)<\/script>/i)
if (nextDataMatch) {
try {
const nextData = JSON.parse(nextDataMatch[1])
const sessionUser = nextData?.props?.pageProps?.initialState?.global?.sessionUser?.user
if (sessionUser && sessionUser.name) {
username = sessionUser.name
avatar = sessionUser.avatar_url || ''
}
} catch (e) {
console.log(`[COSE] ${platformId} 解析 NEXT_DATA 失败:`, e.message)
}
}
// 思否:只有成功提取到用户名才算已登录
if (!username) {
console.log(`[COSE] ${platformId} 未登录或无法获取用户信息`)
return { loggedIn: false }
}
console.log(`[COSE] ${platformId} 用户信息:`, username, avatar ? '有头像' : '无头像')
}
// 微信公众号用户信息提取
else {
else if (platformId === 'wechat') {
// 从 HTML 中提取公众号名称
const nameMatch = html.match(/nick_name\s*[:=]\s*["']([^"']+)["']/i) ||
html.match(/<span[^>]*class="nickname"[^>]*>([^<]+)<\/span>/i)
@@ -749,6 +781,41 @@ function fillContentOnPage(content, platformId) {
console.log('[COSE] 头条未找到编辑器')
}
}
// 思否 SegmentFault
else if (host.includes('segmentfault.com')) {
// 填充标题
const titleInput = await waitFor('input#title, input[placeholder*="标题"]')
if (titleInput) {
titleInput.focus()
titleInput.value = title
titleInput.dispatchEvent(new Event('input', { bubbles: true }))
titleInput.dispatchEvent(new Event('change', { bubbles: true }))
console.log('[COSE] 思否标题填充成功')
} else {
console.log('[COSE] 思否未找到标题输入框')
}
// 等待编辑器加载
await new Promise(resolve => setTimeout(resolve, 1000))
// 思否使用 CodeMirror 编辑器
const cmElement = document.querySelector('.CodeMirror')
if (cmElement && cmElement.CodeMirror) {
cmElement.CodeMirror.setValue(contentToFill)
console.log('[COSE] 思否 CodeMirror 填充成功')
} else {
// 降级到 textarea
const textarea = document.querySelector('textarea')
if (textarea) {
textarea.focus()
textarea.value = contentToFill
textarea.dispatchEvent(new Event('input', { bubbles: true }))
console.log('[COSE] 思否 textarea 填充成功')
} else {
console.log('[COSE] 思否 未找到编辑器')
}
}
}
// 通用处理
else {
const titleSelectors = ['input[placeholder*="标题"]', 'input[name="title"]', 'textarea[placeholder*="标题"]']
+16 -11
View File
@@ -68,6 +68,7 @@
{ id: 'wechat', name: 'WeChat', icon: 'https://res.wx.qq.com/a/wx_fed/assets/res/NTI4MWU5.ico', title: '微信公众号', type: 'wechat' },
{ id: 'zhihu', name: 'Zhihu', icon: 'https://static.zhihu.com/heifetz/favicon.ico', title: '知乎', type: 'zhihu' },
{ id: 'toutiao', name: 'Toutiao', icon: 'https://sf3-cdn-tos.toutiaostatic.com/obj/eden-cn/uhbfnupkbps/toutiao_favicon.ico', title: '今日头条', type: 'toutiao' },
{ id: 'segmentfault', name: 'SegmentFault', icon: 'https://youke2.picui.cn/s1/2025/12/21/6947b2935a41e.ico', title: '思否', type: 'segmentfault' },
]
// 暴露 $cose 全局对象
@@ -93,17 +94,21 @@
const result = await sendMessage('CHECK_PLATFORM_STATUS', { platforms: PLATFORMS })
const status = result?.status || {}
const accounts = PLATFORMS.map(p => ({
uid: p.id,
type: p.type,
title: p.title,
displayName: status[p.id]?.username || p.title,
icon: p.icon,
avatar: status[p.id]?.avatar,
home: '',
checked: false,
loggedIn: status[p.id]?.loggedIn || false,
}))
const accounts = PLATFORMS.map(p => {
const platformStatus = status[p.id] || {}
const isLoggedIn = platformStatus.loggedIn || false
return {
uid: p.id,
type: p.type,
title: p.title,
displayName: isLoggedIn ? (platformStatus.username || p.title) : p.title,
icon: p.icon,
avatar: platformStatus.avatar,
home: '',
checked: false,
loggedIn: isLoggedIn,
}
})
if (typeof callback === 'function') {
callback(accounts)
+17
View File
@@ -47,6 +47,15 @@ const PLATFORMS = [
title: '今日头条',
type: 'toutiao',
},
{
id: 'segmentfault',
name: 'SegmentFault',
icon: 'https://static.segmentfault.com/main_site_next/prod/favicon.ico',
url: 'https://segmentfault.com',
publishUrl: 'https://segmentfault.com/write',
title: '思否',
type: 'segmentfault',
},
]
// 登录检测配置
@@ -94,6 +103,13 @@ const LOGIN_CHECK_CONFIG = {
avatar: response?.data?.media?.https_avatar_url,
}),
},
segmentfault: {
useCookie: true,
cookieUrl: 'https://segmentfault.com',
cookieNames: ['PHPSESSID'],
fetchUserInfoFromPage: true,
userInfoUrl: 'https://segmentfault.com/write',
},
}
// 根据 hostname 获取平台填充函数
@@ -103,6 +119,7 @@ function getPlatformFiller(hostname) {
if (hostname.includes('mp.weixin.qq.com')) return 'wechat'
if (hostname.includes('zhihu.com')) return 'zhihu'
if (hostname.includes('toutiao.com')) return 'toutiao'
if (hostname.includes('segmentfault.com')) return 'segmentfault'
return 'generic'
}
+61
View File
@@ -0,0 +1,61 @@
// 思否平台配置
const SegmentFaultPlatform = {
id: 'segmentfault',
name: 'SegmentFault',
icon: 'https://static.segmentfault.com/main_site_next/prod/favicon.ico',
url: 'https://segmentfault.com',
publishUrl: 'https://segmentfault.com/write',
title: '思否',
type: 'segmentfault',
}
// 思否登录检测配置
const SegmentFaultLoginConfig = {
useCookie: true,
cookieUrl: 'https://segmentfault.com',
cookieNames: ['PHPSESSID'],
fetchUserInfoFromPage: true,
userInfoUrl: 'https://segmentfault.com/write',
}
// 思否内容填充函数
async function fillSegmentFaultContent(content, waitFor, setInputValue) {
const { title, body, markdown } = content
const contentToFill = markdown || body || ''
// 填充标题
const titleInput = await waitFor('input#title, input[placeholder*="标题"]')
if (titleInput) {
titleInput.focus()
titleInput.value = title
titleInput.dispatchEvent(new Event('input', { bubbles: true }))
titleInput.dispatchEvent(new Event('change', { bubbles: true }))
console.log('[COSE] 思否标题填充成功')
}
// 等待编辑器加载
await new Promise(resolve => setTimeout(resolve, 1000))
// 思否使用 CodeMirror 编辑器
const cmElement = document.querySelector('.CodeMirror')
if (cmElement && cmElement.CodeMirror) {
cmElement.CodeMirror.setValue(contentToFill)
console.log('[COSE] 思否 CodeMirror 填充成功')
} else {
// 降级到 textarea
const textarea = document.querySelector('textarea')
if (textarea) {
textarea.focus()
textarea.value = contentToFill
textarea.dispatchEvent(new Event('input', { bubbles: true }))
console.log('[COSE] 思否 textarea 填充成功')
} else {
console.log('[COSE] 思否 未找到编辑器')
}
}
}
// 导出
if (typeof module !== 'undefined' && module.exports) {
module.exports = { SegmentFaultPlatform, SegmentFaultLoginConfig, fillSegmentFaultContent }
}