From ccf477e35159abc881988bc3719cfab77c564f13 Mon Sep 17 00:00:00 2001 From: Neo Date: Sat, 7 Feb 2026 17:29:48 +0800 Subject: [PATCH] refactor: debugging extension loading (#156) * refactor: debugging extension loading * revert: header svg --- {icons => apps/extension/icons}/cose.png | Bin {icons => apps/extension/icons}/cose_128.png | Bin {icons => apps/extension/icons}/cose_16.png | Bin {icons => apps/extension/icons}/cose_48.png | Bin manifest.json => apps/extension/manifest.json | 0 apps/extension/package.json | 30 + apps/extension/scripts/cli.ts | 260 +++++ .../extension/scripts}/convert-icons.mjs | 0 apps/extension/scripts/reload-extension.mjs | 123 ++ {src => apps/extension/src}/background.js | 1033 +++++++++-------- {src => apps/extension/src}/content.js | 0 {src => apps/extension/src}/inject.js | 0 apps/extension/tsconfig.json | 14 + .../extension/vite.config.js | 6 + package.json | 23 +- packages/core/index.js | 4 + packages/core/package.json | 15 + .../core/src}/platforms/alipayopen.js | 16 +- .../core/src}/platforms/aliyun.js | 10 +- .../core/src}/platforms/baijiahao.js | 10 +- .../core/src}/platforms/bilibili.js | 13 +- .../core/src}/platforms/cnblogs.js | 13 +- packages/core/src/platforms/common.js | 3 + {src => packages/core/src}/platforms/csdn.js | 32 +- packages/core/src/platforms/cto51.js | 71 ++ .../core/src}/platforms/douyin.js | 14 +- packages/core/src/platforms/elecfans.js | 9 + .../core/src}/platforms/huaweidev.js | 10 +- packages/core/src/platforms/index.js | 114 ++ {src => packages/core/src}/platforms/infoq.js | 20 +- .../core/src}/platforms/jianshu.js | 15 +- .../core/src}/platforms/juejin.js | 13 +- .../core/src}/platforms/medium.js | 9 +- .../core/src}/platforms/modelscope.js | 14 +- .../core/src}/platforms/oschina.js | 50 +- .../core/src}/platforms/qianfan.js | 10 +- .../core/src}/platforms/segmentfault.js | 11 +- {src => packages/core/src}/platforms/sohu.js | 10 +- {src => packages/core/src}/platforms/sspai.js | 11 +- .../core/src}/platforms/tencentcloud.js | 9 +- .../core/src}/platforms/toutiao.js | 13 +- .../core/src}/platforms/twitter.js | 10 +- packages/core/src/platforms/volcengine.js | 15 + .../core/src}/platforms/wangyihao.js | 11 +- .../core/src}/platforms/wechat.js | 12 +- packages/core/src/platforms/weibo.js | 13 + .../core/src}/platforms/xiaohongshu.js | 13 +- {src => packages/core/src}/platforms/zhihu.js | 13 +- .../common.js => packages/core/src/utils.js | 0 packages/detection/index.js | 368 ++++++ packages/detection/package.json | 11 + pnpm-lock.yaml | 473 +++++++- pnpm-workspace.yaml | 3 +- src/platforms/cto51.js | 148 --- src/platforms/elecfans.js | 25 - src/platforms/huaweicloud.js | 21 - src/platforms/index.js | 147 --- src/platforms/volcengine.js | 25 - src/platforms/weibo.js | 21 - 59 files changed, 2089 insertions(+), 1248 deletions(-) rename {icons => apps/extension/icons}/cose.png (100%) rename {icons => apps/extension/icons}/cose_128.png (100%) rename {icons => apps/extension/icons}/cose_16.png (100%) rename {icons => apps/extension/icons}/cose_48.png (100%) rename manifest.json => apps/extension/manifest.json (100%) create mode 100644 apps/extension/package.json create mode 100644 apps/extension/scripts/cli.ts rename {scripts => apps/extension/scripts}/convert-icons.mjs (100%) create mode 100644 apps/extension/scripts/reload-extension.mjs rename {src => apps/extension/src}/background.js (92%) rename {src => apps/extension/src}/content.js (100%) rename {src => apps/extension/src}/inject.js (100%) create mode 100644 apps/extension/tsconfig.json rename vite.config.js => apps/extension/vite.config.js (86%) create mode 100644 packages/core/index.js create mode 100644 packages/core/package.json rename {src => packages/core/src}/platforms/alipayopen.js (88%) rename {src => packages/core/src}/platforms/aliyun.js (85%) rename {src => packages/core/src}/platforms/baijiahao.js (91%) rename {src => packages/core/src}/platforms/bilibili.js (64%) rename {src => packages/core/src}/platforms/cnblogs.js (81%) create mode 100644 packages/core/src/platforms/common.js rename {src => packages/core/src}/platforms/csdn.js (75%) create mode 100644 packages/core/src/platforms/cto51.js rename {src => packages/core/src}/platforms/douyin.js (50%) create mode 100644 packages/core/src/platforms/elecfans.js rename {src => packages/core/src}/platforms/huaweidev.js (54%) create mode 100644 packages/core/src/platforms/index.js rename {src => packages/core/src}/platforms/infoq.js (75%) rename {src => packages/core/src}/platforms/jianshu.js (82%) rename {src => packages/core/src}/platforms/juejin.js (87%) rename {src => packages/core/src}/platforms/medium.js (86%) rename {src => packages/core/src}/platforms/modelscope.js (50%) rename {src => packages/core/src}/platforms/oschina.js (53%) rename {src => packages/core/src}/platforms/qianfan.js (60%) rename {src => packages/core/src}/platforms/segmentfault.js (83%) rename {src => packages/core/src}/platforms/sohu.js (66%) rename {src => packages/core/src}/platforms/sspai.js (85%) rename {src => packages/core/src}/platforms/tencentcloud.js (94%) rename {src => packages/core/src}/platforms/toutiao.js (79%) rename {src => packages/core/src}/platforms/twitter.js (96%) create mode 100644 packages/core/src/platforms/volcengine.js rename {src => packages/core/src}/platforms/wangyihao.js (85%) rename {src => packages/core/src}/platforms/wechat.js (95%) create mode 100644 packages/core/src/platforms/weibo.js rename {src => packages/core/src}/platforms/xiaohongshu.js (57%) rename {src => packages/core/src}/platforms/zhihu.js (94%) rename src/platforms/common.js => packages/core/src/utils.js (100%) create mode 100644 packages/detection/index.js create mode 100644 packages/detection/package.json delete mode 100644 src/platforms/cto51.js delete mode 100644 src/platforms/elecfans.js delete mode 100644 src/platforms/huaweicloud.js delete mode 100644 src/platforms/index.js delete mode 100644 src/platforms/volcengine.js delete mode 100644 src/platforms/weibo.js diff --git a/icons/cose.png b/apps/extension/icons/cose.png similarity index 100% rename from icons/cose.png rename to apps/extension/icons/cose.png diff --git a/icons/cose_128.png b/apps/extension/icons/cose_128.png similarity index 100% rename from icons/cose_128.png rename to apps/extension/icons/cose_128.png diff --git a/icons/cose_16.png b/apps/extension/icons/cose_16.png similarity index 100% rename from icons/cose_16.png rename to apps/extension/icons/cose_16.png diff --git a/icons/cose_48.png b/apps/extension/icons/cose_48.png similarity index 100% rename from icons/cose_48.png rename to apps/extension/icons/cose_48.png diff --git a/manifest.json b/apps/extension/manifest.json similarity index 100% rename from manifest.json rename to apps/extension/manifest.json diff --git a/apps/extension/package.json b/apps/extension/package.json new file mode 100644 index 0000000..b76cfab --- /dev/null +++ b/apps/extension/package.json @@ -0,0 +1,30 @@ +{ + "name": "cose-extension", + "version": "1.2.2", + "description": "Create Once, Sync Everywhere. 一键将文章同步到多个平台", + "type": "module", + "scripts": { + "dev": "mkdir -p /tmp/chrome-debug-profile && web-ext run --source-dir ./dist --target=chromium --chromium-binary \"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome\" --chromium-profile /tmp/chrome-debug-profile --keep-profile-changes", + "dev:chrome": "mkdir -p /tmp/chrome-debug-profile && '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome' --remote-debugging-port=9222 --user-data-dir=/tmp/chrome-debug-profile --load-extension=$(pwd)/dist", + "dev:watch": "node scripts/reload-extension.mjs", + "build": "tsx scripts/cli.ts build", + "build:firefox": "tsx scripts/cli.ts build --target firefox", + "build:safari": "tsx scripts/cli.ts build --target safari", + "build:release": "tsx scripts/cli.ts build --release", + "watch": "tsx scripts/cli.ts build --watch", + "lint": "web-ext lint --source-dir ./dist" + }, + "dependencies": { + "@cose/core": "workspace:*" + }, + "devDependencies": { + "cac": "^6.7.14", + "execa": "^9.6.1", + "rimraf": "^5.0.5", + "tsx": "^4.21.0", + "vite": "^5.0.12", + "vite-plugin-static-copy": "^1.0.1", + "web-ext": "^7.11.0", + "ws": "^8.19.0" + } +} \ No newline at end of file diff --git a/apps/extension/scripts/cli.ts b/apps/extension/scripts/cli.ts new file mode 100644 index 0000000..2d43da2 --- /dev/null +++ b/apps/extension/scripts/cli.ts @@ -0,0 +1,260 @@ +import { cac } from 'cac' +import { execa } from 'execa' +import { build as viteBuild } from 'vite' +import fs from 'node:fs/promises' +import path from 'node:path' +import { fileURLToPath } from 'node:url' + +const dirname = fileURLToPath(new URL('./', import.meta.url)) +const rootDir = path.join(dirname, '..') + +// Read package.json for version +const packageJsonPath = path.join(rootDir, 'package.json') +const packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf8')) + +// Firefox background uses scripts array, Chrome uses service_worker +interface FirefoxBackgroundOptions { + scripts: string[] + type: 'module' +} + +interface ChromeBackgroundOptions { + service_worker: string + type: 'module' +} + +// Full manifest type for COSE extension +interface Manifest { + manifest_version: number + name: string + version: string + description: string + permissions: string[] + host_permissions: string[] + action: { + default_icon: Record + default_title: string + } + background: FirefoxBackgroundOptions | ChromeBackgroundOptions + content_scripts: Array<{ + matches: string[] + js: string[] + run_at: string + }> + icons: Record + web_accessible_resources: Array<{ + resources: string[] + matches: string[] + }> + browser_specific_settings?: { + gecko: { + id: string + } + } +} + +const readManifest = async (manifestPath: string): Promise => { + try { + const fileContent = await fs.readFile(manifestPath, 'utf8') + const json = JSON.parse(fileContent) as Manifest + return json + } catch (error) { + console.error(error) + return undefined + } +} + +interface BuildOptions { + watch: boolean + release: boolean + target: 'chromium' | 'firefox' | 'safari' + bundleId?: string +} + +const buildWithVite = async (options: BuildOptions) => { + console.log('Building with Vite...') + + await viteBuild({ + root: rootDir, + mode: options.release ? 'production' : 'development', + build: { + minify: options.release, + watch: options.watch ? {} : null, + }, + }) +} + +const copyResources = async () => { + console.log('Copying resources...') + + interface CopyEntry { + from: string + to: string + } + + const copyEntries: CopyEntry[] = [ + { + from: path.join(rootDir, 'icons'), + to: path.join(rootDir, 'dist/icons'), + }, + { + from: path.join(rootDir, 'assets'), + to: path.join(rootDir, 'dist/assets'), + }, + { + // Copy platform scripts from core package + from: path.resolve(rootDir, '../../packages/core/src/platforms'), + to: path.join(rootDir, 'dist/src/platforms'), + }, + ] + + for (const entry of copyEntries) { + try { + // Check if source exists + await fs.access(entry.from) + // Remove destination if exists + await fs.rm(entry.to, { recursive: true, force: true }) + // Copy + await fs.cp(entry.from, entry.to, { recursive: true }) + console.log(` ✓ Copied ${path.basename(entry.from)}`) + } catch (error) { + // Source doesn't exist, skip + console.log(` ⚠ Skipped ${path.basename(entry.from)} (not found)`) + } + } +} + +const genManifest = async (options: BuildOptions) => { + console.log('Generating manifest.json...') + + const manifest = await readManifest(path.join(rootDir, 'manifest.json')) + + if (!manifest) { + throw new Error('manifest.json not found') + } + + if (!manifest.background) { + throw new Error('manifest.background not found') + } + + // Firefox-specific adjustments + if (options.target === 'firefox' && 'service_worker' in manifest.background) { + // Convert service_worker to scripts array for Firefox + manifest.background = { + scripts: [manifest.background.service_worker], + type: 'module', + } + + // Add Firefox-specific settings + manifest.browser_specific_settings = { + gecko: { + id: 'cose@doocs.org', + }, + } + + console.log(' ✓ Converted to Firefox manifest format') + } + + // Sync version from package.json + manifest.version = packageJson.version + + // Write manifest to dist + const outputPath = path.join(rootDir, 'dist/manifest.json') + await fs.writeFile( + outputPath, + JSON.stringify(manifest, null, options.release ? undefined : 2) + ) + + console.log(` ✓ Generated manifest.json (version: ${manifest.version})`) +} + +const buildSafariExtension = async (options: BuildOptions) => { + console.log('\nConverting to Safari extension...') + + // Check if xcrun is available (macOS only) + try { + await execa('xcrun', ['--version']) + } catch { + throw new Error( + 'xcrun not found. Safari extension conversion requires:\n' + + ' 1. macOS\n' + + ' 2. Xcode installed (with Command Line Tools)\n' + + ' 3. Run: xcode-select --install' + ) + } + + const safariProjectDir = path.join(rootDir, 'safari-extension') + const bundleId = options.bundleId || 'org.doocs.cose' + + // Remove existing Safari project + await fs.rm(safariProjectDir, { recursive: true, force: true }) + + console.log(` Bundle ID: ${bundleId}`) + console.log(` Project location: ${safariProjectDir}`) + + try { + const result = await execa('xcrun', [ + 'safari-web-extension-converter', + path.join(rootDir, 'dist'), + '--project-location', safariProjectDir, + '--app-name', 'COSE', + '--bundle-identifier', bundleId, + '--swift', + '--no-prompt', + '--no-open' + ]) + console.log(result.stdout) + console.log('\n ✓ Safari extension project created!') + console.log(` ✓ Open in Xcode: open ${safariProjectDir}/COSE/COSE.xcodeproj`) + } catch (error: unknown) { + const err = error as { stderr?: string; message?: string } + console.error('Safari conversion failed:', err.stderr || err.message) + throw error + } +} + +// CLI setup +const cli = cac('cose-build') +cli.help().version(packageJson.version) + +cli + .command('build', 'Build the COSE browser extension') + .option('-w, --watch', 'Watch mode', { default: false }) + .option('-r, --release', 'Build in release mode with optimizations', { default: false }) + .option('--target ', 'Browser target: "chromium", "firefox", or "safari"', { default: 'chromium' }) + .option('--bundle-id ', 'Bundle ID for Safari (default: org.doocs.cose)') + .action(async (options: BuildOptions) => { + const validTargets = ['chromium', 'firefox', 'safari'] + if (!validTargets.includes(options.target)) { + throw new Error(`Invalid target: ${options.target}. Use "chromium", "firefox", or "safari".`) + } + + console.log(`\n=== COSE Build (target: ${options.target}, release: ${options.release}) ===\n`) + + // Step 1: Build with Vite + await buildWithVite(options) + + // Step 2: Copy resources + await copyResources() + + // Step 3: Generate manifest + await genManifest(options) + + // Step 4: Target-specific post-processing + if (options.target === 'firefox') { + console.log('\nRunning web-ext lint...') + try { + const result = await execa('pnpm', ['exec', 'web-ext', 'lint', '--source-dir', 'dist']) + console.log(result.stdout) + } catch (error) { + console.error('web-ext lint failed:', error) + } + } else if (options.target === 'safari') { + await buildSafariExtension(options) + } + + console.log(`\n=== Build complete! ===\n`) + }) + +cli.parse(process.argv, { run: false }) +await cli.runMatchedCommand() diff --git a/scripts/convert-icons.mjs b/apps/extension/scripts/convert-icons.mjs similarity index 100% rename from scripts/convert-icons.mjs rename to apps/extension/scripts/convert-icons.mjs diff --git a/apps/extension/scripts/reload-extension.mjs b/apps/extension/scripts/reload-extension.mjs new file mode 100644 index 0000000..1341efd --- /dev/null +++ b/apps/extension/scripts/reload-extension.mjs @@ -0,0 +1,123 @@ +#!/usr/bin/env node +/** + * 监听 dist 目录变化,自动刷新 Chrome 扩展 + */ + +import { watch } from 'fs' +import { join, dirname } from 'path' +import { fileURLToPath } from 'url' +import WebSocket from 'ws' + +const __dirname = dirname(fileURLToPath(import.meta.url)) +const distDir = join(__dirname, '..', 'dist') +const CDP_URL = 'http://127.0.0.1:9222' + +let reloadTimeout = null + +async function reloadExtension() { + try { + const res = await fetch(`${CDP_URL}/json/list`) + const pages = await res.json() + + const extPage = pages.find(p => p.url.includes('chrome://extensions')) + if (!extPage) { + console.log('[reload] 未找到 chrome://extensions 页面,请打开该页面') + return + } + + console.log('[reload] 正在重新加载扩展...') + + const ws = new WebSocket(extPage.webSocketDebuggerUrl) + + await new Promise((resolve) => { + ws.on('open', () => { + // 在 extensions 页面中找到 COSE 扩展并点击刷新按钮 + ws.send(JSON.stringify({ + id: 1, + method: 'Runtime.evaluate', + params: { + expression: ` + (async () => { + // 获取 extensions-manager + const manager = document.querySelector('extensions-manager'); + if (!manager) return 'no-manager'; + + // 获取 extensions-item-list + const itemList = manager.shadowRoot.querySelector('extensions-item-list'); + if (!itemList) return 'no-item-list'; + + // 获取所有扩展卡片 + const items = itemList.shadowRoot.querySelectorAll('extensions-item'); + + for (const item of items) { + const name = item.shadowRoot.querySelector('#name')?.textContent || ''; + if (name.includes('COSE') || name.includes('多平台')) { + // 找到刷新按钮并点击 + const reloadBtn = item.shadowRoot.querySelector('#dev-reload-button'); + if (reloadBtn) { + reloadBtn.click(); + return 'ok'; + } + return 'no-reload-btn'; + } + } + return 'not-found'; + })() + `, + awaitPromise: true + } + })) + }) + + ws.on('message', (data) => { + const msg = JSON.parse(data.toString()) + if (msg.id === 1) { + const result = msg.result?.result?.value + if (result === 'ok') { + console.log('[reload] ✓ 扩展已重新加载') + } else if (result === 'no-reload-btn') { + console.log('[reload] ⚠ 未找到刷新按钮,请开启 Developer mode') + } else if (result === 'not-found') { + console.log('[reload] ⚠ 未找到 COSE 扩展') + } else { + console.log('[reload] ⚠ 刷新失败:', result) + } + ws.close() + resolve() + } + }) + + ws.on('error', (e) => { + console.log('[reload] 连接错误:', e.message) + resolve() + }) + + setTimeout(() => { ws.close(); resolve() }, 3000) + }) + } catch (e) { + console.log('[reload] 失败:', e.message) + } +} + +function debounceReload() { + if (reloadTimeout) clearTimeout(reloadTimeout) + reloadTimeout = setTimeout(reloadExtension, 800) +} + +console.log(`[reload] 监听 ${distDir} 目录变化...`) +console.log('[reload] 确保:') +console.log(' 1. Chrome 已用 --remote-debugging-port=9222 启动') +console.log(' 2. chrome://extensions 页面已打开') +console.log(' 3. Developer mode 已开启') + +watch(distDir, { recursive: true }, (eventType, filename) => { + if (filename && !filename.includes('.DS_Store')) { + console.log(`[reload] 检测到变化: ${filename}`) + debounceReload() + } +}) + +process.on('SIGINT', () => { + console.log('\n[reload] 已停止') + process.exit(0) +}) diff --git a/src/background.js b/apps/extension/src/background.js similarity index 92% rename from src/background.js rename to apps/extension/src/background.js index 1a5bdfa..fe47c45 100644 --- a/src/background.js +++ b/apps/extension/src/background.js @@ -1,6 +1,6 @@ // 平台配置 -import { PLATFORMS, LOGIN_CHECK_CONFIG, SYNC_HANDLERS } from './platforms/index.js' -// [DISABLED] import { fillAlipayOpenContent } from './platforms/alipayopen.js' +import { PLATFORMS, LOGIN_CHECK_CONFIG, SYNC_HANDLERS } from '@cose/core/src/platforms/index.js' +// [DISABLED] import { fillAlipayOpenContent } from '@cose/core/src/platforms/alipayopen.js' // 点击扩展图标时打开 md.doocs.org chrome.action.onClicked.addListener(() => { @@ -141,7 +141,7 @@ async function checkAllPlatformsProgressive(platforms, tabId) { try { const result = await checkPlatformLogin(platform) completed++ - + // 通过 content script 发送单个平台结果回页面 if (tabId) { try { @@ -157,12 +157,12 @@ async function checkAllPlatformsProgressive(platforms, tabId) { console.log('[COSE] 发送平台状态更新失败:', platform.id, e.message) } } - + return { id: platform.id, result } } catch (e) { completed++ const errorResult = { loggedIn: false, error: e.message } - + if (tabId) { try { await chrome.tabs.sendMessage(tabId, { @@ -177,14 +177,14 @@ async function checkAllPlatformsProgressive(platforms, tabId) { console.log('[COSE] 发送平台状态更新失败:', platform.id, e2.message) } } - + return { id: platform.id, result: errorResult } } }) // 等待所有完成后发送完成消息 await Promise.allSettled(promises) - + if (tabId) { try { await chrome.tabs.sendMessage(tabId, { @@ -213,12 +213,12 @@ async function checkPlatformLogin(platform) { // 从 storage 读取缓存的用户信息 const stored = await chrome.storage.local.get('alipayopen_user') const cachedUser = stored.alipayopen_user - + if (cachedUser && cachedUser.loggedIn) { // 检查缓存是否过期(1小时) const cacheAge = Date.now() - (cachedUser.cachedAt || 0) const maxAge = 1 * 60 * 60 * 1000 // 1 hour - + if (cacheAge < maxAge) { console.log(`[COSE] alipayopen 从缓存读取用户信息:`, cachedUser.username) return { @@ -232,13 +232,13 @@ async function checkPlatformLogin(platform) { await chrome.storage.local.remove('alipayopen_user') } } - + // 尝试从已打开的支付宝页面获取用户信息并缓存 let tabs = await chrome.tabs.query({ url: 'https://open.alipay.com/*' }) if (tabs.length === 0) { tabs = await chrome.tabs.query({ url: 'https://*.alipay.com/*' }) } - + if (tabs.length > 0) { try { // 在已打开的页面上下文中调用 API @@ -262,14 +262,14 @@ async function checkPlatformLogin(platform) { } } }) - + const data = results?.[0]?.result console.log(`[COSE] alipayopen API 数据:`, data) - + if (data?.stat === 'ok' && data?.data?.isLoginUser === 1) { const username = data.data.nickname || '' const avatar = data.data.avatar || '' - + // 缓存用户信息 await chrome.storage.local.set({ alipayopen_user: { @@ -279,7 +279,7 @@ async function checkPlatformLogin(platform) { cachedAt: Date.now() } }) - + console.log(`[COSE] alipayopen 用户信息:`, username, avatar ? '有头像' : '无头像') return { loggedIn: true, username, avatar } } @@ -287,7 +287,7 @@ async function checkPlatformLogin(platform) { console.log(`[COSE] alipayopen 从页面获取用户信息失败:`, e.message) } } - + console.log(`[COSE] alipayopen 未检测到登录状态,请先访问支付宝开放平台`) return { loggedIn: false } } catch (e) { @@ -304,24 +304,24 @@ async function checkPlatformLogin(platform) { url: 'https://card.weibo.com', name: 'SUBP' }) - + // 也检查 ALF cookie const alfCookie = await chrome.cookies.get({ url: 'https://card.weibo.com', name: 'ALF' }) - + console.log(`[COSE] weibo cookies: SUBP=${!!subpCookie}, ALF=${!!alfCookie}`) - + if (!subpCookie && !alfCookie) { console.log(`[COSE] weibo 未找到登录 cookie,未登录`) return { loggedIn: false } } - + // 有 cookie,通过 fetch HTML 获取用户信息 let username = '' let avatar = '' - + try { // 获取所有相关 cookies 并手动添加到请求 const weiboCookies = await chrome.cookies.getAll({ domain: '.weibo.com' }) @@ -329,9 +329,9 @@ async function checkPlatformLogin(platform) { const sinaCookies = await chrome.cookies.getAll({ domain: '.sina.com.cn' }) const allCookies = [...weiboCookies, ...cardCookies, ...sinaCookies] const cookieString = allCookies.map(c => `${c.name}=${c.value}`).join('; ') - + console.log(`[COSE] weibo 获取到 ${allCookies.length} 个 cookies`) - + const response = await fetch('https://card.weibo.com/article/v5/editor', { method: 'GET', headers: { @@ -340,15 +340,15 @@ async function checkPlatformLogin(platform) { credentials: 'include', }) const html = await response.text() - + console.log(`[COSE] weibo HTML 长度:`, html.length) - + // 从 HTML 中提取用户名 const nickMatch = html.match(/"nick"\s*:\s*"([^"]+)"/) if (nickMatch) { username = nickMatch[1] } - + // 从 HTML 中提取头像 const avatarMatch = html.match(/"avatar_large"\s*:\s*"([^"]+)"/) if (avatarMatch) { @@ -359,18 +359,18 @@ async function checkPlatformLogin(platform) { avatar = rawAvatar.split('?')[0] } } - + console.log(`[COSE] weibo 用户信息:`, username, avatar ? '有头像' : '无头像') } catch (e) { console.log(`[COSE] weibo 获取用户详情失败:`, e.message) } - + // 如果没有获取到用户名,说明实际上未登录或登录已过期 if (!username) { console.log(`[COSE] weibo 未获取到用户名,视为未登录`) return { loggedIn: false } } - + return { loggedIn: true, username, avatar } } catch (e) { console.log(`[COSE] weibo 检测失败:`, e.message) @@ -388,11 +388,11 @@ async function checkPlatformLogin(platform) { // 先检查缓存 const stored = await chrome.storage.local.get('xiaohongshu_user') const cachedUser = stored.xiaohongshu_user - + if (cachedUser && cachedUser.loggedIn) { const cacheAge = Date.now() - (cachedUser.cachedAt || 0) const maxAge = 7 * 24 * 60 * 60 * 1000 // 7 days - + if (cacheAge < maxAge) { console.log(`[COSE] xiaohongshu 从缓存读取:`, cachedUser.username) return { @@ -404,7 +404,7 @@ async function checkPlatformLogin(platform) { await chrome.storage.local.remove('xiaohongshu_user') } } - + // 缓存无效,尝试在已打开的小红书页面中检测 const tabs = await chrome.tabs.query({ url: 'https://creator.xiaohongshu.com/*' }) if (tabs.length > 0) { @@ -417,9 +417,9 @@ async function checkPlatformLogin(platform) { credentials: 'include', headers: { 'Accept': 'application/json' } }) - + if (!response.ok) return null - + const data = await response.json() if (data?.success === true && data?.code === 0 && data?.data?.userId) { return { @@ -435,7 +435,7 @@ async function checkPlatformLogin(platform) { } } }) - + const result = results?.[0]?.result if (result && result.loggedIn) { // 缓存结果 @@ -452,7 +452,7 @@ async function checkPlatformLogin(platform) { } } } - + console.log(`[COSE] xiaohongshu 未登录,请先访问小红书创作者中心`) return { loggedIn: false } } catch (e) { @@ -469,19 +469,19 @@ async function checkPlatformLogin(platform) { 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(), { @@ -491,15 +491,15 @@ async function checkPlatformLogin(platform) { '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 || '' @@ -529,7 +529,10 @@ async function checkPlatformLogin(platform) { const response = await fetch(config.api, { method: config.method || 'GET', credentials: 'include', - headers: { 'Accept': 'application/json', 'Cache-Control': 'no-cache' }, + headers: { + 'Accept': config.isHtml ? 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8' : 'application/json', + 'Cache-Control': 'no-cache' + }, signal: controller.signal, }) clearTimeout(timeoutId) @@ -537,7 +540,9 @@ async function checkPlatformLogin(platform) { let data = null const contentType = response.headers.get('content-type') || '' - if (contentType.includes('application/json') || contentType.includes('text/plain')) { + if (config.isHtml || contentType.includes('text/html')) { + try { data = await response.text() } catch (e) { data = '' } + } else if (contentType.includes('application/json') || contentType.includes('text/plain')) { try { data = await response.json() } catch (e) { data = null } } console.log(`[COSE] ${platform.id} API 数据:`, data) @@ -623,7 +628,7 @@ async function checkLoginByCookie(platformId, config) { } }) const data = await response.json() - + if (data.errno === 0 && data.data?.user?.username) { username = data.data.user.username || data.data.user.name avatar = data.data.user.avatar || '' @@ -655,7 +660,7 @@ async function checkLoginByCookie(platformId, config) { } }) const data = await response.json() - + if (data.code === 1 && data.data?.tname) { username = data.data.tname avatar = data.data.icon || '' @@ -680,12 +685,12 @@ async function checkLoginByCookie(platformId, config) { url: 'https://mp.sohu.com', name: 'ppinf' }) - + if (!ppinfCookie || !ppinfCookie.value) { console.log(`[COSE] ${platformId} 未找到 ppinf cookie,未登录`) return { loggedIn: false } } - + // 直接调用 API 获取用户信息 try { const response = await fetch('https://mp.sohu.com/mpbp/bp/account/list', { @@ -696,7 +701,7 @@ async function checkLoginByCookie(platformId, config) { } }) const data = await response.json() - + if (data.success && data.data?.data?.[0]?.accounts?.[0]) { const account = data.data.data[0].accounts[0] let avatar = account.avatar || '' @@ -704,10 +709,10 @@ async function checkLoginByCookie(platformId, config) { avatar = 'https:' + avatar } console.log(`[COSE] ${platformId} 用户信息:`, account.nickName, avatar ? '有头像' : '无头像') - return { - loggedIn: true, - username: account.nickName, - avatar + return { + loggedIn: true, + username: account.nickName, + avatar } } else { console.log(`[COSE] ${platformId} API 返回无用户数据`) @@ -732,12 +737,12 @@ async function checkLoginByCookie(platformId, config) { url: 'https://developer.aliyun.com', name: 'login_aliyunid_ticket' }) - + if (!ticketCookie || !ticketCookie.value) { console.log(`[COSE] ${platformId} 未找到 login_aliyunid_ticket cookie,未登录`) return { loggedIn: false } } - + // 调用 API 获取用户信息 const response = await fetch('https://developer.aliyun.com/developer/api/my/user/getUser', { method: 'GET', @@ -747,7 +752,7 @@ async function checkLoginByCookie(platformId, config) { } }) const data = await response.json() - + if (data.success && data.data?.nickname) { const username = data.data.nickname const avatar = data.data.avatar || '' @@ -768,18 +773,18 @@ async function checkLoginByCookie(platformId, config) { try { // 查找华为云相关页面(优先 bbs,其次其他华为云页面) let tabs = await chrome.tabs.query({ url: 'https://bbs.huaweicloud.com/*' }) - + if (tabs.length === 0) { tabs = await chrome.tabs.query({ url: 'https://*.huaweicloud.com/*' }) } - + if (tabs.length === 0) { console.log(`[COSE] ${platformId} 没有打开的华为云页面`) return { loggedIn: false } } - + console.log(`[COSE] ${platformId} 使用页面:`, tabs[0].url) - + // 在页面上下文中调用 API(使用同步包装) try { const results = await chrome.scripting.executeScript({ @@ -792,38 +797,38 @@ async function checkLoginByCookie(platformId, config) { credentials: 'include', headers: { 'Accept': 'application/json', 'csrf': csrf } }) - .then(response => response.ok ? response.json() : null) - .then(data => { - if (data && data.memName) { - resolve({ memName: data.memName, memAlias: data.memAlias, memPhoto: data.memPhoto }) - } else { - resolve(null) - } - }) - .catch(() => resolve(null)) + .then(response => response.ok ? response.json() : null) + .then(data => { + if (data && data.memName) { + resolve({ memName: data.memName, memAlias: data.memAlias, memPhoto: data.memPhoto }) + } else { + resolve(null) + } + }) + .catch(() => resolve(null)) }) } }) - + // executeScript 返回的 result 可能是 Promise,需要 await let data = results?.[0]?.result if (data && typeof data.then === 'function') { data = await data } - + if (data && data.memName) { console.log(`[COSE] ${platformId} 已登录:`, data.memName) - return { - loggedIn: true, - username: data.memAlias || data.memName, - avatar: data.memPhoto || '' + return { + loggedIn: true, + username: data.memAlias || data.memName, + avatar: data.memPhoto || '' } } console.log(`[COSE] ${platformId} API 返回:`, data) } catch (e) { console.log(`[COSE] ${platformId} scripting 失败:`, e.message) } - + return { loggedIn: false } } catch (e) { console.log(`[COSE] ${platformId} 检测失败:`, e.message) @@ -839,14 +844,14 @@ async function checkLoginByCookie(platformId, config) { url: 'https://developer.huawei.com', name: 'developer_userinfo' }) - + if (!userInfoCookie || !userInfoCookie.value) { console.log(`[COSE] ${platformId} 未找到 developer_userinfo cookie,未登录`) return { loggedIn: false } } - + console.log(`[COSE] ${platformId} 找到 developer_userinfo cookie`) - + // 解析 cookie 获取 csrfToken let csrfToken = '' try { @@ -855,7 +860,7 @@ async function checkLoginByCookie(platformId, config) { } catch (e) { console.log(`[COSE] ${platformId} 解析 cookie 失败:`, e.message) } - + // 如果没有 csrfToken,尝试从单独的 cookie 获取 if (!csrfToken) { const csrfCookie = await chrome.cookies.get({ @@ -864,17 +869,17 @@ async function checkLoginByCookie(platformId, config) { }) csrfToken = csrfCookie?.value || '' } - + if (!csrfToken) { console.log(`[COSE] ${platformId} 未找到 csrfToken,返回已登录但无用户信息`) return { loggedIn: true, username: '', avatar: '' } } - + // 通过 API 获取用户信息 const now = new Date() const hdDate = now.toISOString().replace(/[-:]/g, '').replace(/\.\d{3}/, '') const serialNo = Math.floor(Math.random() * 10000000).toString() - + const response = await fetch('https://svc-drcn.developer.huawei.com/codeserver/Common/v1/delegate', { method: 'POST', credentials: 'include', @@ -893,9 +898,9 @@ async function checkLoginByCookie(platformId, config) { reqJson: JSON.stringify({ getNickName: '1' }) }) }) - + const data = await response.json() - + if (data.returnCode === '0' && data.resJson) { const userInfo = JSON.parse(data.resJson) const username = userInfo.displayName || userInfo.loginID || '' @@ -921,12 +926,12 @@ async function checkLoginByCookie(platformId, config) { url: 'https://sspai.com', name: 'sspai_jwt_token' }) - + if (!jwtCookie || !jwtCookie.value) { console.log(`[COSE] ${platformId} 未找到 JWT token`) return { loggedIn: false } } - + const token = jwtCookie.value const response = await fetch('https://sspai.com/api/v1/user/info/get', { method: 'GET', @@ -937,7 +942,7 @@ async function checkLoginByCookie(platformId, config) { } }) const data = await response.json() - + if (data.error === 0 && data.data?.nickname) { username = data.data.nickname // 少数派 CDN 没有跨域限制,直接使用原始头像 URL @@ -963,33 +968,33 @@ async function checkLoginByCookie(platformId, config) { }) const html = await response.text() const finalUrl = response.url - + // 检查是否被重定向到首页(未登录时会重定向) if (!finalUrl.includes('/creator')) { console.log(`[COSE] ${platformId} 未登录:被重定向到 ${finalUrl}`) return { loggedIn: false } } - + // 检查页面是否包含登录按钮(未登录标志) if (html.includes('登录/注册') || html.includes('"isLogin":false') || html.includes('"login":false')) { console.log(`[COSE] ${platformId} 未登录:页面包含登录按钮`) return { loggedIn: false } } - + // 从创作中心页面提取当前用户信息(通常在页面的用户信息区域) // 匹配 "userInfo" 或 "creatorInfo" 等包含当前用户信息的 JSON 对象 const userInfoMatch = html.match(/"userInfo"\s*:\s*\{[^}]*"nickname"\s*:\s*"([^"]+)"[^}]*\}/) || - html.match(/"creatorInfo"\s*:\s*\{[^}]*"nickname"\s*:\s*"([^"]+)"[^}]*\}/) || - html.match(/"currentUser"\s*:\s*\{[^}]*"nickname"\s*:\s*"([^"]+)"[^}]*\}/) - + html.match(/"creatorInfo"\s*:\s*\{[^}]*"nickname"\s*:\s*"([^"]+)"[^}]*\}/) || + html.match(/"currentUser"\s*:\s*\{[^}]*"nickname"\s*:\s*"([^"]+)"[^}]*\}/) + // 备用方案:匹配创作中心特有的用户信息结构 const creatorNicknameMatch = html.match(/class="creator-info[^"]*"[^>]*>[\s\S]*?<[^>]*class="[^"]*name[^"]*"[^>]*>([^<]+) const shortName = username.replace(/\d+$/, '') // 移除末尾数字,如 fwen925 -> fwen const avatarPattern = new RegExp(`]*alt="${shortName}"[^>]*src="([^"]+)"`) const avatarMatch = html.match(avatarPattern) - + // 备用: src 在 alt 之前 const avatarPattern2 = new RegExp(`]*src="([^"]+)"[^>]*alt="${shortName}"`) const avatarMatch2 = html.match(avatarPattern2) - + avatar = (avatarMatch && avatarMatch[1]) || (avatarMatch2 && avatarMatch2[1]) || '' - + console.log(`[COSE] ${platformId} 用户信息:`, username, avatar ? '有头像' : '无头像') return { loggedIn: true, username, avatar } } else { @@ -1092,19 +1097,19 @@ async function checkLoginByCookie(platformId, config) { url: 'https://x.com', name: 'ct0' }) - + // 如果没有 auth_token cookie,说明未登录 if (!authTokenCookie) { console.log(`[COSE] ${platformId} 未找到 auth_token cookie,未登录`) return { loggedIn: false } } - + console.log(`[COSE] ${platformId} 找到登录 cookie: auth_token=${!!authTokenCookie}, ct0=${!!ct0Cookie}`) - + // 尝试获取用户信息 let username = '' let avatar = '' - + try { // 方法1: 从 Twitter 首页 HTML 中提取用户信息 const response = await fetch('https://x.com/home', { @@ -1114,29 +1119,29 @@ async function checkLoginByCookie(platformId, config) { 'Accept': 'text/html', } }) - + if (response.ok) { const html = await response.text() - + // 从页面脚本中提取 screen_name const screenNameMatch = html.match(/"screen_name"\s*:\s*"([^"]+)"/) if (screenNameMatch) { username = screenNameMatch[1] } - + // 从页面脚本中提取头像 URL const avatarMatch = html.match(/"profile_image_url_https"\s*:\s*"([^"]+)"/) if (avatarMatch) { // 将 _normal 替换为 _x96 获取更大的头像 avatar = avatarMatch[1].replace('_normal.', '_x96.') } - + console.log(`[COSE] ${platformId} 用户信息:`, username, avatar ? '有头像' : '无头像') } } catch (e) { console.log(`[COSE] ${platformId} 获取用户信息失败:`, e.message) } - + // 有 auth_token cookie 就认为已登录 return { loggedIn: true, username, avatar } } catch (e) { @@ -1156,20 +1161,20 @@ async function checkLoginByCookie(platformId, config) { 'Accept': 'application/json', } }) - + if (!response.ok) { console.log(`[COSE] ${platformId} API 请求失败: ${response.status}`) return { loggedIn: false } } - + const data = await response.json() console.log(`[COSE] ${platformId} API 响应:`, data) - + // 检查 API 返回是否成功且有用户信息 if (data.success && data.result) { const username = data.result.displayName || data.result.nickname || '' const avatar = data.result.avatar || '' - + console.log(`[COSE] ${platformId} 用户信息: ${username}, ${avatar ? '有头像' : '无头像'}`) return { loggedIn: true, username, avatar } } else { @@ -1425,7 +1430,7 @@ async function syncToPlatform(platformId, content) { const initialUrl = platformId === 'wechat' ? 'https://mp.weixin.qq.com/' : platform.publishUrl tab = await chrome.tabs.create({ url: initialUrl, active: false }) await addTabToSyncGroup(tab.id, tab.windowId) - + // 调用平台特定处理器 const helpers = { chrome, @@ -1451,12 +1456,12 @@ async function syncToPlatform(platformId, content) { } }) const data = await response.json() - + if (data.code === 0 && data.data?.id) { const draftId = data.data.id const targetUrl = `https://xie.infoq.cn/draft/${draftId}` console.log('[COSE] InfoQ 创建草稿成功,ID:', draftId) - + tab = await chrome.tabs.create({ url: targetUrl, active: false }) await addTabToSyncGroup(tab.id, tab.windowId) await waitForTab(tab.id) @@ -1480,15 +1485,15 @@ async function syncToPlatform(platformId, content) { } }) const notebooks = await notebooksResp.json() - + if (!notebooks || notebooks.length === 0) { return { success: false, message: '简书未找到文集,请先创建一个文集' } } - + // 使用第一个文集 const notebookId = notebooks[0].id console.log('[COSE] 简书使用文集:', notebooks[0].name, 'ID:', notebookId) - + // 创建新文章 const createResp = await fetch('https://www.jianshu.com/author/notes', { method: 'POST', @@ -1504,12 +1509,12 @@ async function syncToPlatform(platformId, content) { }) }) const noteData = await createResp.json() - + if (noteData && noteData.id) { const noteId = noteData.id const targetUrl = `https://www.jianshu.com/writer#/notebooks/${notebookId}/notes/${noteId}` console.log('[COSE] 简书创建文章成功,ID:', noteId) - + tab = await chrome.tabs.create({ url: targetUrl, active: false }) await addTabToSyncGroup(tab.id, tab.windowId) await waitForTab(tab.id) @@ -1524,69 +1529,69 @@ async function syncToPlatform(platformId, content) { } else if (platformId === 'xiaohongshu') { // 小红书:需要先点击"新的创作"按钮,等待编辑器加载后填充 console.log('[COSE] 开始处理小红书同步...') - + // 打开发布页面 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, 3000)) - + // 在页面中执行:点击"新的创作"并等待编辑器加载 const clickResult = await chrome.scripting.executeScript({ target: { tabId: tab.id }, func: async () => { const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms)) - + // 查找"新的创作"按钮 const createBtn = Array.from(document.querySelectorAll('button')) .find(el => el.textContent.includes('新的创作')) - + if (createBtn) { createBtn.click() console.log('[COSE] 小红书已点击"新的创作"按钮') - + // 等待编辑器加载(等待富文本编辑器出现) const waitForEditor = async (timeout = 10000) => { const start = Date.now() while (Date.now() - start < timeout) { // 查找编辑器元素,可能是 contenteditable 或 textarea const editor = document.querySelector('[contenteditable="true"]') || - document.querySelector('textarea') || - document.querySelector('.editor') || - document.querySelector('.content-editor') + document.querySelector('textarea') || + document.querySelector('.editor') || + document.querySelector('.content-editor') if (editor) return true await sleep(200) } return false } - + const editorLoaded = await waitForEditor() return { success: editorLoaded, message: editorLoaded ? 'Editor loaded' : 'Editor timeout' } } - + return { success: false, message: 'Create button not found' } } }) - + if (!clickResult[0]?.result?.success) { return { success: false, message: '小红书创建文章失败: ' + (clickResult[0]?.result?.message || '未知错误') } } - + // 等待页面稳定 await new Promise(resolve => setTimeout(resolve, 1000)) - + // 使用剪贴板 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: async (title, htmlBody) => { const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms)) - + // 等待元素出现的工具函数 const waitForElement = (selector, timeout = 15000) => { return new Promise((resolve) => { @@ -1608,10 +1613,10 @@ async function syncToPlatform(platformId, content) { }, timeout) }) } - + try { console.log('[COSE] 小红书开始填充内容...') - + // 等待并查找标题输入框 const titleInput = await waitForElement('input[placeholder*="标题"], textarea[placeholder*="标题"], .title-input', 5000) if (titleInput && title) { @@ -1627,39 +1632,39 @@ async function syncToPlatform(platformId, content) { titleInput.dispatchEvent(new Event('change', { bubbles: true })) console.log('[COSE] 小红书标题已填充:', title) } - + // 稍等一下让标题生效 await new Promise(r => setTimeout(r, 300)) - + // 等待并查找内容编辑器 const contentEditor = await waitForElement('[contenteditable="true"], .editor-content, .content-editor', 5000) if (contentEditor && htmlBody) { contentEditor.focus() - + // 清空现有占位符内容 - if (contentEditor.textContent.includes('从这里开始写正文') || - contentEditor.textContent.includes('请输入正文') || - contentEditor.textContent.includes('写点什么')) { + if (contentEditor.textContent.includes('从这里开始写正文') || + contentEditor.textContent.includes('请输入正文') || + contentEditor.textContent.includes('写点什么')) { contentEditor.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 }) - + contentEditor.dispatchEvent(pasteEvent) console.log('[COSE] 小红书内容已通过 paste 事件注入') - + // 等待内容渲染 await new Promise(r => setTimeout(r, 500)) - + // 验证内容是否注入成功 const wordCount = contentEditor.textContent?.length || 0 if (wordCount === 0) { @@ -1667,10 +1672,10 @@ async function syncToPlatform(platformId, content) { console.log('[COSE] paste 事件未生效,尝试备用方案') contentEditor.innerHTML = htmlBody } - + return { success: true, method: 'paste-html', length: htmlBody.length } } - + return { success: false, error: 'Content editor not found' } } catch (e) { console.error('[COSE] 小红书同步失败:', e) @@ -1680,21 +1685,21 @@ async function syncToPlatform(platformId, content) { args: [content.title, htmlContent], world: 'MAIN', }) - + console.log('[COSE] 小红书填充结果:', fillResult[0]?.result) - + // 等待内容注入完成 await new Promise(resolve => setTimeout(resolve, 1000)) - + return { success: true, message: '已同步到小红书', tabId: tab.id } } else if (platformId === 'twitter') { // Twitter Articles:需要先打开草稿列表页,然后点击 create 按钮创建新文章 // 注意:Twitter 使用 Page Visibility API,后台标签页不会渲染编辑器 // 解决方案:短暂激活 Tab,等编辑器加载后切回原 Tab - + // 记录当前活动的 Tab const [currentTab] = await chrome.tabs.query({ active: true, currentWindow: true }) - + // 第一步:打开草稿列表页(激活状态,触发编辑器渲染) tab = await chrome.tabs.create({ url: platform.publishUrl, active: true }) await addTabToSyncGroup(tab.id, tab.windowId) @@ -1708,17 +1713,17 @@ async function syncToPlatform(platformId, content) { target: { tabId: tab.id }, func: async () => { const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms)) - + // 查找 create 按钮 const createBtn = document.querySelector('button[aria-label="create"]') || - Array.from(document.querySelectorAll('button')).find(b => - b.getAttribute('aria-label')?.toLowerCase() === 'create' - ) - + Array.from(document.querySelectorAll('button')).find(b => + b.getAttribute('aria-label')?.toLowerCase() === 'create' + ) + if (createBtn) { createBtn.click() console.log('[COSE] Twitter Articles 已点击 create 按钮') - + // 等待编辑器加载(等待标题输入框出现) const waitForEditor = async (timeout = 10000) => { const start = Date.now() @@ -1729,11 +1734,11 @@ async function syncToPlatform(platformId, content) { } return false } - + const editorLoaded = await waitForEditor() return { success: editorLoaded, message: editorLoaded ? 'Editor loaded' : 'Editor timeout' } } - + return { success: false, message: 'Create button not found' } }, world: 'MAIN', @@ -1768,7 +1773,7 @@ async function syncToPlatform(platformId, content) { func: async (title, markdown) => { // ========== 工具函数 ========== const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms)) - + const waitForElement = async (selector, timeout = 10000) => { const start = Date.now() while (Date.now() - start < timeout) { @@ -1786,73 +1791,73 @@ async function syncToPlatform(platformId, content) { // 代码块使用样式化 HTML,公式使用 CodeCogs API 渲染为图片 function parseMarkdownToHtml(md) { if (!md) return '' - + // 存储需要保护的内容 const codeBlocks = [] const inlineCodes = [] const blockFormulas = [] const inlineFormulas = [] - + let html = md - + // ========== 第一阶段:提取并保护特殊内容 ========== - + // 1. 提取代码块 ```...``` html = html.replace(/```(\w*)\n?([\s\S]*?)```/g, (match, lang, code) => { const index = codeBlocks.length codeBlocks.push({ lang: lang || '', code: code }) return `__CODE_BLOCK_${index}__` }) - + // 2. 提取行内代码 `...` html = html.replace(/`([^`\n]+)`/g, (match, code) => { const index = inlineCodes.length inlineCodes.push(code) return `__INLINE_CODE_${index}__` }) - + // 3. 提取块级公式 $$...$$ html = html.replace(/\$\$([\s\S]+?)\$\$/g, (match, formula) => { const index = blockFormulas.length blockFormulas.push(formula.trim()) return `__BLOCK_FORMULA_${index}__` }) - + // 4. 提取行内公式 $...$ html = html.replace(/\$([^\$\n]+)\$/g, (match, formula) => { const index = inlineFormulas.length inlineFormulas.push(formula.trim()) return `__INLINE_FORMULA_${index}__` }) - + // ========== 第二阶段:处理标准 Markdown 语法 ========== - + // 处理标题 html = html.replace(/^#### (.+)$/gm, '

$1

') html = html.replace(/^### (.+)$/gm, '

$1

') html = html.replace(/^## (.+)$/gm, '

$1

') html = html.replace(/^# (.+)$/gm, '

$1

') - + // 处理引用块 html = html.replace(/^> (.+)$/gm, '
$1
') - + // 处理水平分割线 html = html.replace(/^---$/gm, '
') html = html.replace(/^\*\*\*$/gm, '
') - + // 处理图片 html = html.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '$1') - + // 处理链接 html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1') - + // 处理粗体、斜体、删除线 html = html.replace(/\*\*([^*]+)\*\*/g, '$1') html = html.replace(/\*([^*]+)\*/g, '$1') html = html.replace(/~~([^~]+)~~/g, '$1') - + // ========== 第三阶段:恢复保护的内容 ========== - + // 恢复代码块(使用样式化的 pre/code) codeBlocks.forEach((block, index) => { const escapedCode = block.code @@ -1861,13 +1866,13 @@ async function syncToPlatform(platformId, content) { .replace(/>/g, '>') .replace(/"/g, '"') .replace(/'/g, ''') - + const langLabel = block.lang ? `
${block.lang}
` : '' const codeHtml = `
${langLabel}
${escapedCode}
` - + html = html.replace(`__CODE_BLOCK_${index}__`, codeHtml) }) - + // 恢复行内代码 inlineCodes.forEach((code, index) => { const escapedCode = code @@ -1875,75 +1880,75 @@ async function syncToPlatform(platformId, content) { .replace(//g, '>') const codeHtml = `${escapedCode}` - + html = html.replace(`__INLINE_CODE_${index}__`, codeHtml) }) - + // 恢复块级公式(使用 CodeCogs API 渲染为图片) blockFormulas.forEach((formula, index) => { const encodedFormula = encodeURIComponent(formula) const formulaHtml = `
${formula.replace(/
` - + html = html.replace(`__BLOCK_FORMULA_${index}__`, formulaHtml) }) - + // 恢复行内公式 inlineFormulas.forEach((formula, index) => { const encodedFormula = encodeURIComponent(formula) const formulaHtml = `${formula.replace(/` - + html = html.replace(`__INLINE_FORMULA_${index}__`, formulaHtml) }) - + // ========== 第四阶段:处理列表和段落 ========== - + // 处理无序列表项 html = html.replace(/^[\*\-\+] (.+)$/gm, '
  • $1
  • ') - + // 处理有序列表项 html = html.replace(/^\d+[\.\)] (.+)$/gm, '
  • $1
  • ') - + // 将连续的
  • 包装成