适配器运行在 Chrome Extension Service Worker 中,这是一个受限的 JavaScript 环境。
┌─────────────────────────────────────────────────────────┐
│ Chrome Extension │
│ ┌──────────────────────────────────────────────────┐ │
│ │ Service Worker (Background) │ │
│ │ ┌──────────────────────────────────────────┐ │ │
│ │ │ Platform Adapters │ │ │
│ │ │ - ZhihuAdapter │ │ │
│ │ │ - JuejinAdapter │ │ │
│ │ │ - ... │ │ │
│ │ └──────────────────────────────────────────┘ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌──────────────────────────────────────────┐ │ │
│ │ │ RuntimeInterface │ │ │
│ │ │ - fetch (with cookies) │ │ │
│ │ │ - headerRules (declarativeNetRequest) │ │ │
│ │ │ - storage / session │ │ │
│ │ └──────────────────────────────────────────┘ │ │
│ └──────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
Service Worker 不可用 的 API:
| API | 说明 | 替代方案 |
|---|---|---|
DOMParser |
DOM 解析 | 正则表达式提取 |
document |
文档对象 | 正则表达式 / runtime.dom |
window |
全局窗口对象 | 不可用 |
XMLHttpRequest |
传统 AJAX | fetch API |
localStorage |
本地存储 | runtime.storage |
sessionStorage |
会话存储 | runtime.session |
alert/confirm/prompt |
对话框 | 不可用 |
Image |
图片对象 | fetch + Blob |
Canvas |
画布操作 | 不可用 |
Service Worker 可用 的 API:
| API | 说明 |
|---|---|
fetch |
HTTP 请求 |
Request/Response |
请求响应对象 |
Headers |
HTTP 头部 |
URL/URLSearchParams |
URL 处理 |
FormData |
表单数据 |
Blob/File |
二进制数据 |
FileReader |
文件读取 |
TextEncoder/TextDecoder |
文本编码 |
crypto |
加密 API |
setTimeout/setInterval |
定时器 |
Promise/async-await |
异步编程 |
JSON |
JSON 处理 |
RegExp |
正则表达式 |
PlatformAdapter (interface)
│
▼
CodeAdapter (abstract class)
│
▼
XxxAdapter (concrete class)
适配器不直接调用浏览器 API,而是通过 RuntimeInterface 抽象层:
interface RuntimeInterface {
type: 'extension' | 'node'
// HTTP 请求(自动携带 cookies)
fetch(url: string, options?: RequestInit): Promise<Response>
// Cookie 管理
cookies: {
get(domain: string): Promise<Cookie[]>
set(cookie: Cookie): Promise<void>
remove(name: string, domain: string): Promise<void>
}
// 持久化存储
storage: {
get<T>(key: string): Promise<T | null>
set<T>(key: string, value: T): Promise<void>
remove(key: string): Promise<void>
}
// 会话存储
session: {
get<T>(key: string): Promise<T | null>
set<T>(key: string, value: T): Promise<void>
}
// Header 规则(仅扩展环境,用于 CORS 绕过)
headerRules?: {
add(rule: HeaderRule): Promise<string>
remove(ruleId: string): Promise<void>
clear(): Promise<void>
}
}import { CodeAdapter, ImageUploadResult } from '../code-adapter'
import type { Article, AuthResult, SyncResult, PlatformMeta } from '../../types'
import { createLogger } from '../../lib/logger'
const logger = createLogger('PlatformName')
export class PlatformAdapter extends CodeAdapter {
// 平台元信息(必须)
meta: PlatformMeta = {
id: 'platform-id', // 唯一标识,小写
name: '平台名称', // 显示名称
icon: 'https://...', // 图标 URL
homepage: 'https://...', // 平台首页
capabilities: ['article', 'draft', 'image_upload'],
}
// 私有状态
private someToken: string | null = null
private headerRuleId: string | null = null
// 检查登录状态(必须实现)
async checkAuth(): Promise<AuthResult> { ... }
// 发布文章(必须实现)
async publish(article: Article, options?: PublishOptions): Promise<SyncResult> { ... }
// 上传图片(可选实现)
async uploadImageByUrl(url: string): Promise<ImageUploadResult> { ... }
}禁止使用 DOMParser,必须使用正则表达式:
// ❌ 错误:Service Worker 中不可用
const parser = new DOMParser()
const doc = parser.parseFromString(html, 'text/html')
const element = doc.querySelector('.user-avatar')
// ✅ 正确:使用正则表达式
const userIdMatch = html.match(/data-user-id="(\d+)"/)
const avatarMatch = html.match(/class="avatar"[^>]*src="([^"]+)"/)
if (!userIdMatch) {
return { isAuthenticated: false, error: '未登录' }
}
const userId = userIdMatch[1]某些平台 API 需要特定的 Origin/Referer 头,使用 headerRules 处理:
// 添加规则时必须检查 headerRules 是否存在
private async addHeaderRules(): Promise<void> {
if (!this.runtime.headerRules) return // 必须检查!
this.headerRuleId = await this.runtime.headerRules.add({
urlFilter: '*://api.example.com/*',
headers: {
Origin: 'https://example.com',
Referer: 'https://example.com/',
},
})
}
// 清理规则
private async removeHeaderRules(): Promise<void> {
if (!this.runtime.headerRules) return // 必须检查!
if (this.headerRuleId) {
await this.runtime.headerRules.remove(this.headerRuleId)
this.headerRuleId = null
}
}
// 在 publish 中使用
async publish(article: Article): Promise<SyncResult> {
try {
await this.addHeaderRules()
// ... 发布逻辑
return result
} finally {
await this.removeHeaderRules() // 确保清理
}
}async uploadImageByUrl(url: string): Promise<ImageUploadResult> {
// 1. 下载图片
const imageResponse = await this.runtime.fetch(url)
const blob = await imageResponse.blob()
// 2. 构建 FormData
const formData = new FormData()
const filename = `${Date.now()}.jpg`
formData.append('image', blob, filename)
// 3. 上传
const response = await this.runtime.fetch('https://api.example.com/upload', {
method: 'POST',
credentials: 'include',
body: formData,
})
const res = await response.json()
// 4. 返回结果
if (!res.url) {
throw new Error('图片上传失败')
}
return { url: res.url }
}async publish(article: Article): Promise<SyncResult> {
const now = Date.now()
try {
// ... 发布逻辑
return {
platform: this.meta.id,
success: true,
postId: result.id,
postUrl: `https://example.com/post/${result.id}`,
draftOnly: true, // 是否仅保存草稿
timestamp: now,
}
} catch (error) {
return {
platform: this.meta.id,
success: false,
error: (error as Error).message,
timestamp: now,
}
}
}使用统一的 logger,生产环境自动降级:
import { createLogger } from '../../lib/logger'
const logger = createLogger('PlatformName')
// 调试信息(生产环境不输出)
logger.debug('Processing image:', imageUrl)
// 警告信息
logger.warn('Rate limited, retrying...')
// 错误信息
logger.error('Upload failed:', error)interface PlatformMeta {
id: string // 平台唯一标识
name: string // 显示名称
icon: string // 图标 URL
homepage: string // 平台首页 URL
capabilities: Array<'article' | 'draft' | 'image_upload' | 'video'>
}interface AuthResult {
isAuthenticated: boolean
userId?: string
username?: string
avatar?: string
error?: string
}interface Article {
title: string
html?: string // HTML 格式内容
markdown?: string // Markdown 格式内容
cover?: string // 封面图 URL
summary?: string // 摘要
tags?: string[] // 标签
}interface SyncResult {
platform: string // 平台 ID
success: boolean
postId?: string // 文章 ID
postUrl?: string // 文章 URL
draftOnly?: boolean // 是否仅草稿
error?: string
timestamp: number
}编辑 packages/core/src/adapters/platforms/index.ts:
export { NewPlatformAdapter } from './new-platform'编辑 packages/extension/src/adapters/index.ts:
// 导入
import {
// ...existing imports
NewPlatformAdapter,
} from '@wechatsync/core'
// 添加到适配器列表
const ADAPTER_CLASSES = [
// ...existing adapters
NewPlatformAdapter,
] as constprivate async getCsrfToken(): Promise<string> {
const response = await this.runtime.fetch('https://example.com/editor', {
credentials: 'include',
})
const html = await response.text()
const tokenMatch = html.match(/csrf[_-]?token["']?\s*[:=]\s*["']([^"']+)["']/i)
if (!tokenMatch) {
throw new Error('获取 CSRF token 失败')
}
return tokenMatch[1]
}private async getPageConfig(): Promise<PageConfig> {
const response = await this.runtime.fetch('https://example.com/write', {
credentials: 'include',
})
const html = await response.text()
// 提取嵌入的 JSON 配置
const configMatch = html.match(/window\.__CONFIG__\s*=\s*(\{[\s\S]*?\});/)
if (!configMatch) {
throw new Error('获取页面配置失败')
}
return JSON.parse(configMatch[1])
}async checkAuth(): Promise<AuthResult> {
const response = await this.runtime.fetch('https://example.com/api/user?callback=cb', {
credentials: 'include',
})
let text = await response.text()
// 去除 JSONP 包装
text = text.replace(/^cb\(/, '').replace(/\);?$/, '')
const result = JSON.parse(text)
// ...
}- 构建扩展:
pnpm build - Chrome 加载
packages/extension/dist目录 - 打开目标平台并登录
- 测试同步功能
- Service Worker 控制台:
chrome://extensions-> 点击 "Service Worker" - 网络请求:Service Worker DevTools -> Network 面板
- 存储数据:Application -> Storage -> Extension Storage
| 环境 | 版本要求 |
|---|---|
| Chrome | >= 110 (MV3 支持) |
| Node.js | >= 18 (MCP Server) |
| TypeScript | >= 5.0 |
推荐参考这些实现良好的适配器:
| 适配器 | 特点 |
|---|---|
zhihu.ts |
完整的 Header 规则管理 |
juejin.ts |
Markdown 优先 + 图片上传 |
bilibili.ts |
多步骤发布流程 |
csdn.ts |
复杂的 API 签名 |