Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
# Changelog

## v2.0.9 (2026-06-12)

- 🔧 修复 MCP Server 连接稳定性,新增 WebSocket 心跳保活机制
- 🔧 优化 MCP 重连策略:前 5 次快速轮询 + 指数退避 + 本地服务器智能识别
- 🔧 新增 MCP_RECONNECT 手动重连指令,设置页支持显示连接状态和错误信息
- 🔧 新增 Chrome MV3 Service Worker 定期唤醒机制(每 30s),确保扩展后台常驻
- 🔧 延长默认连接超时至 45 秒
- 🔧 要求 minimum_chrome_version >= 120
- 🧪 新增 MCP Client 单元测试

## v2.0.8 (2026-03-17)

- 🆕 新增抖音图文
Expand Down
4 changes: 2 additions & 2 deletions packages/cli/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,13 @@ const GITHUB_URL = 'https://github.com/wechatsync/Wechatsync'
const program = new Command()

// 默认超时时间
let connectionTimeout = 30000
let connectionTimeout = 45000

program
.name('wechatsync')
.description('同步文章到多个内容平台 (知乎、掘金、CSDN 等)')
.version('1.1.0')
.option('--timeout <ms>', '等待 Extension 连接超时(毫秒)', '30000')
.option('--timeout <ms>', '等待 Extension 连接超时(毫秒)', '45000')
.hook('preAction', (thisCommand) => {
const opts = thisCommand.opts()
if (opts.timeout) {
Expand Down
2 changes: 2 additions & 0 deletions packages/extension/__tests__/mcp-client.test.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export {};
//# sourceMappingURL=mcp-client.test.d.ts.map
1 change: 1 addition & 0 deletions packages/extension/__tests__/mcp-client.test.d.ts.map

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

113 changes: 113 additions & 0 deletions packages/extension/__tests__/mcp-client.test.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/extension/__tests__/mcp-client.test.js.map

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

160 changes: 160 additions & 0 deletions packages/extension/__tests__/mcp-client.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { chromeMock, mockStorage } from '../vitest.setup'

vi.mock('../src/adapters', () => ({
checkAllPlatformsAuth: vi.fn(),
checkPlatformAuth: vi.fn(),
getAdapter: vi.fn(),
}))

vi.mock('../src/background/sync-service', () => ({
performSync: vi.fn(),
}))

import { McpClient } from '../src/mcp/client'

class MockWebSocket {
static readonly CONNECTING = 0
static readonly OPEN = 1
static readonly CLOSING = 2
static readonly CLOSED = 3

static instances: MockWebSocket[] = []

readyState = MockWebSocket.CONNECTING
onopen: (() => void) | null = null
onmessage: ((event: { data: string }) => void) | null = null
onclose: ((event: { code: number; reason: string }) => void) | null = null
onerror: (() => void) | null = null
sent: string[] = []

constructor(readonly url: string) {
MockWebSocket.instances.push(this)
}

open(): void {
this.readyState = MockWebSocket.OPEN
this.onopen?.()
}

fail(code = 1006, reason = ''): void {
this.readyState = MockWebSocket.CLOSED
this.onerror?.()
this.onclose?.({ code, reason })
}

send(data: string): void {
this.sent.push(data)
}

close(): void {
this.readyState = MockWebSocket.CLOSED
this.onclose?.({ code: 1000, reason: '' })
}
}

async function flushPromises(): Promise<void> {
await Promise.resolve()
await Promise.resolve()
}

describe('McpClient connection lifecycle', () => {
beforeEach(() => {
vi.useFakeTimers()
MockWebSocket.instances = []
mockStorage.mcpEnabled = true
vi.stubGlobal('WebSocket', MockWebSocket)
})

afterEach(() => {
vi.useRealTimers()
vi.unstubAllGlobals()
})

it('does not replace an in-progress connection', () => {
const client = new McpClient()

client.connect()
client.connect()

expect(MockWebSocket.instances).toHaveLength(1)
expect(client.getStatus().connecting).toBe(true)
})

it('retries quickly before switching to the cold backoff', async () => {
const client = new McpClient()
const delays = [500, 1000, 2000, 4000, 5000, 10000]

client.setServerUrl('ws://192.0.2.1:9527')
client.connect()

for (const delay of delays) {
MockWebSocket.instances.at(-1)!.fail()
await flushPromises()

const countBeforeRetry = MockWebSocket.instances.length
await vi.advanceTimersByTimeAsync(delay - 1)
expect(MockWebSocket.instances).toHaveLength(countBeforeRetry)

await vi.advanceTimersByTimeAsync(1)
expect(MockWebSocket.instances).toHaveLength(countBeforeRetry + 1)
}

client.disconnect()
})

it('keeps localhost retries within five seconds', async () => {
const client = new McpClient()
const delays = [500, 1000, 2000, 4000, 5000, 5000]

client.connect()

for (const delay of delays) {
MockWebSocket.instances.at(-1)!.fail()
await flushPromises()
await vi.advanceTimersByTimeAsync(delay)
}

expect(MockWebSocket.instances).toHaveLength(delays.length + 1)
client.disconnect()
})

it('keeps the service worker active while waiting for the local CLI', async () => {
const client = new McpClient()

client.connect()

await vi.advanceTimersByTimeAsync(20001)

expect(globalThis.chrome.runtime.getPlatformInfo).toHaveBeenCalledTimes(1)

client.disconnect()
})

it('immediately reconnects when reset while a retry is pending', async () => {
const client = new McpClient()

client.connect()
MockWebSocket.instances[0].fail()
await flushPromises()

client.resetReconnect()

expect(MockWebSocket.instances).toHaveLength(2)
client.disconnect()
})

it('checks the enabled flag before scheduling a retry', async () => {
const client = new McpClient()
mockStorage.mcpEnabled = false

client.connect()
MockWebSocket.instances[0].fail()
await flushPromises()
await vi.advanceTimersByTimeAsync(10000)

expect(chromeMock.storage.local.get).toHaveBeenCalledWith('mcpEnabled')
expect(MockWebSocket.instances).toHaveLength(1)
client.disconnect()
})
})
1 change: 1 addition & 0 deletions packages/extension/manifest.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"manifest_version": 3,
"minimum_chrome_version": "120",
"name": "文章同步助手",
"description": "一键同步文章到知乎、头条、掘金等 20+ 平台,支持 WordPress 等自建站",
"version": "2.0.9",
Expand Down
Loading