├── src ├── utils │ ├── apiManager.js │ └── cookieManager.js ├── icons │ ├── icon16.png │ ├── icon48.png │ └── icon128.png ├── contents │ ├── contentScript.js │ └── extractArticle.js ├── adapters │ ├── adapters.js │ ├── ZhiHuAdapter.js │ ├── CnblogAdapter.js │ ├── BilibiliAdapter.js │ ├── EmlogAdapter.js │ ├── DiscuzAdapter.js │ ├── WeiboAdapter.js │ └── WordPressAdapter.js ├── core │ ├── BaseAdapter.js │ ├── getCsrfToken.js │ └── syncManager.js ├── sync │ ├── sync.html │ ├── sync.css │ └── sync.js ├── manifest.json ├── options │ ├── options.css │ ├── options.html │ └── options.js ├── popup │ ├── popup.html │ ├── popup.css │ └── popup.js └── background.js ├── images ├── Feeding.gif ├── QQ20241016-162303.png ├── QQ20241016-162333.png ├── QQ20241016-162808.png ├── QQ20241016-162937.png └── QQ20241016-163214.png ├── .gitignore ├── package.json ├── webpack.config.js └── README.md /src/utils/apiManager.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /images/Feeding.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iAJue/Articlesync/HEAD/images/Feeding.gif -------------------------------------------------------------------------------- /src/icons/icon16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iAJue/Articlesync/HEAD/src/icons/icon16.png -------------------------------------------------------------------------------- /src/icons/icon48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iAJue/Articlesync/HEAD/src/icons/icon48.png -------------------------------------------------------------------------------- /src/icons/icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iAJue/Articlesync/HEAD/src/icons/icon128.png -------------------------------------------------------------------------------- /images/QQ20241016-162303.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iAJue/Articlesync/HEAD/images/QQ20241016-162303.png -------------------------------------------------------------------------------- /images/QQ20241016-162333.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iAJue/Articlesync/HEAD/images/QQ20241016-162333.png -------------------------------------------------------------------------------- /images/QQ20241016-162808.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iAJue/Articlesync/HEAD/images/QQ20241016-162808.png -------------------------------------------------------------------------------- /images/QQ20241016-162937.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iAJue/Articlesync/HEAD/images/QQ20241016-162937.png -------------------------------------------------------------------------------- /images/QQ20241016-163214.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iAJue/Articlesync/HEAD/images/QQ20241016-163214.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | node_modules/ 3 | build/ 4 | md/ 5 | build/devtool/ 6 | .DS_Store 7 | *.iml 8 | yarn-error.log 9 | dist 10 | zip 11 | package-json.lock 12 | package-lock.json 13 | 14 | -------------------------------------------------------------------------------- /src/contents/contentScript.js: -------------------------------------------------------------------------------- 1 | import { extractArticle } from './extractArticle'; 2 | chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { 3 | if (message.action === "startExtraction") { 4 | const articleData = extractArticle(); 5 | chrome.runtime.sendMessage({ 6 | action: 'articleExtracted', 7 | data: articleData 8 | }); 9 | } else if (message.action === "checkScript") { 10 | sendResponse({ status: "scriptAlreadyInjected" }); 11 | } 12 | }); -------------------------------------------------------------------------------- /src/utils/cookieManager.js: -------------------------------------------------------------------------------- 1 | export const getCookies = (url, name) => { 2 | return new Promise((resolve, reject) => { 3 | chrome.cookies.get({ url, name }, (cookie) => { 4 | if (cookie) { 5 | resolve(cookie.value); 6 | } else { 7 | reject(`No cookie found for ${name}`); 8 | } 9 | }); 10 | }); 11 | }; 12 | 13 | export const getCookie = (name, cookieStr) => { 14 | const match = cookieStr.match(new RegExp('(^| )' + name + '=([^;]+)')); 15 | if (match) { 16 | return match[2]; 17 | } 18 | return null; 19 | } -------------------------------------------------------------------------------- /src/adapters/adapters.js: -------------------------------------------------------------------------------- 1 | import ZhiHuAdapter from './ZhiHuAdapter'; 2 | import BilibiliAdapter from './BilibiliAdapter'; 3 | import CnblogAdapter from './CnblogAdapter'; 4 | import WeiboAdapter from './WeiboAdapter'; 5 | import EmlogAdapter from './EmlogAdapter'; 6 | import WordPressAdapter from './WordPressAdapter'; 7 | import DiscuzAdapter from './DiscuzAdapter'; 8 | 9 | const adapters = [ 10 | new ZhiHuAdapter(), 11 | new BilibiliAdapter(), 12 | new CnblogAdapter(), 13 | new WeiboAdapter(), 14 | new EmlogAdapter(), 15 | new WordPressAdapter(), 16 | new DiscuzAdapter(), 17 | ]; 18 | 19 | export default adapters; -------------------------------------------------------------------------------- /src/core/BaseAdapter.js: -------------------------------------------------------------------------------- 1 | export default class BaseAdapter { 2 | constructor() { 3 | if (new.target === BaseAdapter) { 4 | throw new Error("Cannot instantiate BaseAdapter directly."); 5 | } 6 | } 7 | 8 | // 获取平台元数据的方法 9 | async getMetaData() { 10 | throw new Error("getMetaData() must be implemented."); 11 | } 12 | 13 | // 发布文章的方法 14 | async addPost(post) { 15 | throw new Error("addPost() must be implemented."); 16 | } 17 | 18 | // 编辑文章的方法 19 | async editPost(post, post_id) { 20 | throw new Error("editPost() must be implemented."); 21 | } 22 | 23 | // 上传文件的方法 24 | async uploadFile(file) { 25 | throw new Error("uploadFile() must be implemented."); 26 | } 27 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "articlesync", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "background.js", 6 | "scripts": { 7 | "clean": "rimraf dist", 8 | "build": "webpack", 9 | "watch": "webpack --watch", 10 | "dev": "webpack serve", 11 | "lint": "eslint ./src", 12 | "prebuild": "npm run clean", 13 | "reload-extension": "chrome-cli reload", 14 | "watch-reload": "npm run watch & nodemon --watch dist --exec 'npm run reload-extension'" 15 | }, 16 | "keywords": [], 17 | "author": "", 18 | "license": "ISC", 19 | "dependencies": { 20 | "@mozilla/readability": "^0.5.0", 21 | "jquery": "^3.7.1", 22 | "marked": "^14.1.3", 23 | "turndown": "^7.2.0" 24 | }, 25 | "devDependencies": { 26 | "copy-webpack-plugin": "^12.0.2", 27 | "eslint": "^9.12.0", 28 | "nodemon": "^3.1.7", 29 | "rimraf": "^6.0.1", 30 | "webpack": "^5.95.0", 31 | "webpack-cli": "^5.1.4", 32 | "webpack-dev-server": "^5.1.0" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/core/getCsrfToken.js: -------------------------------------------------------------------------------- 1 | import { getCookie } from '../utils/cookieManager'; 2 | 3 | // 从指定域名获取指定 Cookie 值 4 | export const getTokenFromCookie = async (domain, cookieName) => { 5 | return new Promise((resolve, reject) => { 6 | chrome.cookies.getAll({ domain: domain }, (cookies) => { 7 | if (cookies) { 8 | const cookieStr = cookies.map(cookie => `${cookie.name}=${cookie.value}`).join('; '); 9 | const token = getCookie(cookieName, cookieStr); 10 | if (token) { 11 | resolve(token); 12 | } else { 13 | reject(`No ${cookieName} found for domain ${domain}`); 14 | } 15 | } else { 16 | reject(`No cookies found for domain ${domain}`); 17 | } 18 | }); 19 | }); 20 | }; 21 | 22 | // 提交处理函数,直接返回 Token 23 | export const getCsrfToken = async (domain, cookieName) => { 24 | try { 25 | const token = await getTokenFromCookie(domain, cookieName); 26 | console.log(`${cookieName} token`, token); 27 | return token; 28 | } catch (error) { 29 | console.error(error); 30 | throw error; 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /src/sync/sync.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 同步文章 7 | 8 | 9 | 10 | 11 |
12 | 13 |
14 | 15 |
获取失败
16 |
17 | 18 | 19 |
20 |

选择发布平台

21 |
22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 |
30 |
31 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /src/contents/extractArticle.js: -------------------------------------------------------------------------------- 1 | import TurndownService from 'turndown'; 2 | import { marked } from 'marked'; 3 | import { Readability } from '@mozilla/readability'; 4 | 5 | // 初始化 TurndownService 6 | const turndownService = new TurndownService(); 7 | 8 | // 提取文章的函数 9 | export function extractArticle() { 10 | const docClone = document.cloneNode(true); 11 | const reader = new Readability(docClone); 12 | const article = reader.parse(); 13 | 14 | if (article) { 15 | return { 16 | title: article.title, 17 | content: article.content 18 | }; 19 | } else { 20 | console.error('无法提取文章'); 21 | return null; 22 | } 23 | } 24 | 25 | // 1. HTML 转 Markdown 26 | export function htmlToMarkdown(htmlContent) { 27 | const markdownContent = turndownService.turndown(htmlContent); 28 | return markdownContent; 29 | } 30 | 31 | // 2. HTML 转 纯文本 32 | export function htmlToText(htmlContent) { 33 | // 替换

标签为换行符 34 | let textContent = htmlContent.replace(//gi, '\n'); 35 | textContent = textContent.replace(/<\/p>/gi, '\n'); 36 | 37 | // 替换多个连续的空格为单个空格,或保留缩进 38 | textContent = textContent.replace(/ /g, ' '); 39 | 40 | // 去除其他 HTML 标签,但保留文本 41 | textContent = textContent.replace(/<[^>]*>/g, ''); 42 | 43 | return textContent; 44 | } 45 | 46 | // 3. Markdown 转 HTML 47 | export function markdownToHtml(markdownContent) { 48 | const htmlContent = marked(markdownContent); 49 | return htmlContent; 50 | } -------------------------------------------------------------------------------- /src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | "name": "文章同步助手", 4 | "version": "1.1", 5 | "description": "一个浏览器扩展程序,用于在多个社交媒体平台上同步您的文章。", 6 | "permissions": [ 7 | "contextMenus", 8 | "activeTab", 9 | "storage", 10 | "scripting", 11 | "declarativeNetRequest", 12 | "declarativeNetRequestWithHostAccess", 13 | "declarativeNetRequestFeedback", 14 | "cookies" 15 | ], 16 | "host_permissions": [ 17 | "" 18 | ], 19 | "background": { 20 | "service_worker": "background.js" 21 | }, 22 | "options_ui": { 23 | "page": "options/options.html", 24 | "open_in_tab": false 25 | }, 26 | "action": { 27 | "default_popup": "popup/popup.html", 28 | "default_icon": { 29 | "16": "icons/icon16.png", 30 | "48": "icons/icon48.png", 31 | "128": "icons/icon128.png" 32 | } 33 | }, 34 | "icons": { 35 | "16": "icons/icon16.png", 36 | "48": "icons/icon48.png", 37 | "128": "icons/icon128.png" 38 | }, 39 | "content_scripts": [ 40 | { 41 | "matches": [ 42 | "" 43 | ], 44 | "js": [ 45 | "contentScript.js" 46 | ] 47 | } 48 | ], 49 | "options_page": "options/options.html", 50 | "web_accessible_resources": [ 51 | { 52 | "resources": [ 53 | "adapters/adapters.js" 54 | ], 55 | "matches": [ 56 | "" 57 | ] 58 | } 59 | ] 60 | } -------------------------------------------------------------------------------- /src/core/syncManager.js: -------------------------------------------------------------------------------- 1 | import adapters from '../adapters/adapters'; 2 | 3 | class SyncManager { 4 | constructor() { 5 | this.adapters = {}; 6 | this.registerAdapters(); 7 | } 8 | 9 | // 动态注册所有适配器 10 | registerAdapters() { 11 | adapters.forEach(adapter => { 12 | this.registerAdapter(adapter); 13 | }); 14 | } 15 | 16 | // 注册单个适配器 17 | registerAdapter(adapter) { 18 | this.adapters[adapter.type] = adapter; 19 | } 20 | 21 | // 获取支持的平台列表 22 | async getSupportedPlatforms() { 23 | const platforms = []; 24 | for (const platformName in this.adapters) { 25 | const adapter = this.adapters[platformName]; 26 | platforms.push({ 27 | value: adapter.type, 28 | label: adapter.name, 29 | }); 30 | } 31 | return platforms; 32 | } 33 | 34 | // 获取平台元数据 35 | async getPlatformMeta(platformName) { 36 | const adapter = this.adapters[platformName]; 37 | if (!adapter) throw new Error(`Adapter for platform ${platformName} not found.`); 38 | return await adapter.getMetaData(); 39 | } 40 | 41 | // 同步文章 42 | async syncPost(platformName, post) { 43 | const adapter = this.adapters[platformName]; 44 | if (!adapter) throw new Error(`Adapter for platform ${platformName} not found.`); 45 | return await adapter.addPost(post); 46 | } 47 | 48 | // 编辑文章 49 | async editPost(platformName, post_id, post) { 50 | const adapter = this.adapters[platformName]; 51 | if (!adapter) throw new Error(`Adapter for platform ${platformName} not found.`); 52 | return await adapter.editPost(post_id, post); 53 | } 54 | } 55 | 56 | export default new SyncManager(); -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const CopyWebpackPlugin = require('copy-webpack-plugin'); 3 | const webpack = require('webpack'); 4 | 5 | module.exports = { 6 | entry: { 7 | contentScript: './src/contents/contentScript.js', 8 | background: './src/background.js', 9 | popup: './src/popup/popup.js', 10 | options: './src/options/options.js', 11 | sync: './src/sync/sync.js', 12 | adapters: './src/adapters/adapters.js', 13 | }, 14 | output: { 15 | filename: (pathData) => { 16 | if (pathData.chunk.name === 'popup') { 17 | return 'popup/[name].js'; 18 | }else if (pathData.chunk.name === 'options') { 19 | return 'options/[name].js'; 20 | }else if (pathData.chunk.name === 'sync') { 21 | return 'sync/[name].js'; 22 | } 23 | return '[name].js'; 24 | }, 25 | path: path.resolve(__dirname, 'dist'), 26 | }, 27 | mode: 'development', 28 | devtool: 'inline-source-map', 29 | watch: true, 30 | plugins: [ 31 | 32 | new CopyWebpackPlugin({ 33 | patterns: [ 34 | { from: 'src/manifest.json', to: 'manifest.json' }, 35 | { from: 'src/icons', to: 'icons' }, 36 | { from: 'src/popup/popup.html', to: 'popup/popup.html' }, 37 | { from: 'src/popup/popup.css', to: 'popup/popup.css' }, 38 | { from: 'src/options/options.html', to: 'options/options.html' }, 39 | { from: 'src/options/options.css', to: 'options/options.css' }, 40 | { from: 'src/sync/sync.html', to: 'sync/sync.html' }, 41 | { from: 'src/sync/sync.css', to: 'sync/sync.css' }, 42 | { from: 'images', to: 'images' }, 43 | ], 44 | }), 45 | new webpack.ProvidePlugin({ 46 | $: 'jquery', 47 | jQuery: 'jquery', 48 | }), 49 | ], 50 | }; -------------------------------------------------------------------------------- /src/options/options.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | margin: 0; 4 | padding: 0; 5 | font-family: Arial, sans-serif; 6 | } 7 | 8 | body { 9 | background-color: #f5f5f5; 10 | color: #333; 11 | } 12 | 13 | .container { 14 | max-width: 600px; 15 | background-color: #fff; 16 | padding: 20px; 17 | box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); 18 | } 19 | 20 | h1 { 21 | text-align: center; 22 | color: #4899f8; 23 | margin-bottom: 20px; 24 | } 25 | 26 | .tabs { 27 | display: flex; 28 | justify-content: space-around; 29 | margin-bottom: 20px; 30 | } 31 | 32 | .tab-button { 33 | background-color: #e0e0e0; 34 | border: none; 35 | padding: 10px 20px; 36 | cursor: pointer; 37 | color: #333; 38 | border-radius: 5px; 39 | transition: background-color 0.3s ease; 40 | } 41 | 42 | .tab-button.active { 43 | background-color: #4899f8; 44 | color: #fff; 45 | } 46 | 47 | .tab-button:hover { 48 | background-color: #4899f8; 49 | color: #fff; 50 | } 51 | 52 | .tab-content { 53 | display: none; 54 | } 55 | 56 | .tab-content.active { 57 | display: block; 58 | } 59 | 60 | .form-group { 61 | margin-bottom: 20px; 62 | } 63 | 64 | .form-group.horizontal { 65 | display: flex; 66 | align-items: center; 67 | justify-content: space-between; 68 | } 69 | 70 | label { 71 | flex-basis: 40%; 72 | font-weight: bold; 73 | } 74 | 75 | input[type="text"], 76 | select, 77 | input[type="checkbox"] { 78 | padding: 10px; 79 | width: 55%; 80 | border: 1px solid #ccc; 81 | border-radius: 5px; 82 | } 83 | 84 | input[type="checkbox"] { 85 | width: auto; 86 | margin-left: 10px; 87 | } 88 | 89 | .save-button { 90 | background-color: #4899f8; 91 | color: #fff; 92 | padding: 10px 20px; 93 | border: none; 94 | border-radius: 5px; 95 | cursor: pointer; 96 | width: 100%; 97 | font-size: 16px; 98 | transition: background-color 0.3s ease; 99 | } 100 | 101 | .save-button:hover { 102 | background-color: #357bb5; 103 | } -------------------------------------------------------------------------------- /src/options/options.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 插件设置 8 | 9 | 10 | 11 | 12 |

13 |
14 | 15 | 16 |
17 |
18 |
19 |
20 | 21 | 24 |
25 |
26 | 27 | 28 |
29 |
30 | 31 | 32 |
33 |
34 | 35 | 40 |
41 |
42 | 43 | 44 |
45 | 46 |
47 |
48 | 49 |
50 |

ArticleSync 是一个浏览器扩展,帮助用户轻松将文章同步发布到多个社交平台。它提供了一站式解决方案,让你在不同的社交媒体平台上同步文章变得简单高效。

51 |
插件版本:1.0.0
52 |
版权信息:© 2024 阿珏酱
53 |
GitHub:https://github.com/iAJue/Articlesync
54 | 55 |
56 |
57 | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /src/popup/popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 插件设置 6 | 7 | 8 | 9 |
10 | 15 |
16 |
17 |
18 |
19 | 22 | 29 |
30 | 59 |
60 | 61 | 62 | 63 | 64 |
65 |
66 | 67 | 68 | -------------------------------------------------------------------------------- /src/background.js: -------------------------------------------------------------------------------- 1 | // 注册右键 2 | chrome.runtime.onInstalled.addListener(() => { 3 | chrome.contextMenus.create({ 4 | id: "sync-article", 5 | title: "同步文章", 6 | contexts: ["page", "selection"] 7 | }); 8 | }); 9 | 10 | chrome.contextMenus.onClicked.addListener((info, tab) => { 11 | // 监控右键点击事件,注入提取文章脚本 12 | if (info.menuItemId === "sync-article") { 13 | chrome.tabs.sendMessage(tab.id, { action: "checkScript" }, (response) => { 14 | if (chrome.runtime.lastError || !response) { 15 | chrome.scripting.executeScript({ 16 | target: { tabId: tab.id }, 17 | files: ['contentScript.js'] 18 | }).then(() => { 19 | chrome.tabs.sendMessage(tab.id, { action: "startExtraction" }); 20 | }).catch((error) => { 21 | console.error("脚本注入失败", error); 22 | }); 23 | } else { 24 | chrome.tabs.sendMessage(tab.id, { action: "startExtraction" }); 25 | } 26 | }); 27 | } 28 | }); 29 | 30 | 31 | chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { 32 | if (message.action === "articleExtracted") { 33 | chrome.storage.local.set({ article: message.data }, () => { 34 | chrome.windows.getCurrent((currentWindow) => { 35 | const width = Math.round(currentWindow.width * 0.85); 36 | const height = Math.round(currentWindow.height * 0.8); 37 | const left = Math.round((currentWindow.width - width) / 2 + currentWindow.left); 38 | const top = Math.round((currentWindow.height - height) / 2 + currentWindow.top); 39 | chrome.tabs.query({ active: true, currentWindow: true }, function (tabs) { 40 | var currentTab = tabs[0]; 41 | chrome.windows.create({ 42 | url: "sync/sync.html?currentUrl=" + encodeURIComponent(currentTab.url), 43 | type: "popup", 44 | width: width, 45 | height: height, 46 | left: left, 47 | top: top 48 | }); 49 | }); 50 | }); 51 | }); 52 | } 53 | }); 54 | 55 | // 注册监听器,监听指定的 URL 请求 56 | chrome.declarativeNetRequest.updateDynamicRules({ 57 | addRules: [ 58 | { 59 | "id": 1, 60 | "priority": 1, 61 | "action": { 62 | "type": "modifyHeaders", 63 | "requestHeaders": [ 64 | { "header": "Origin", "operation": "set", "value": "https://card.weibo.com" }, 65 | { "header": "Referer", "operation": "set", "value": "https://card.weibo.com/article/v3/editor" } 66 | ] 67 | }, 68 | "condition": { 69 | "urlFilter": "https://card.weibo.com/article/v3/*", 70 | "resourceTypes": ["xmlhttprequest"] 71 | } 72 | }, 73 | { 74 | "id": 2, 75 | "priority": 1, 76 | "action": { 77 | "type": "modifyHeaders", 78 | "requestHeaders": [ 79 | { "header": "Origin", "operation": "set", "value": "https://member.bilibili.com" } 80 | ] 81 | }, 82 | "condition": { 83 | "urlFilter": "https://api.bilibili.com/x/article/creative/*", 84 | "resourceTypes": ["xmlhttprequest"] 85 | } 86 | } 87 | ], 88 | removeRuleIds: [1, 2] 89 | }); -------------------------------------------------------------------------------- /src/sync/sync.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: 'Arial', sans-serif; 3 | margin: 0; 4 | padding: 0; 5 | background-color: #FFF; 6 | color: #333; 7 | } 8 | 9 | .container { 10 | display: flex; 11 | flex-direction: row; 12 | height: 100vh; /* 让布局占满整个页面 */ 13 | } 14 | 15 | .left-section { 16 | flex: 3; 17 | padding: 20px; 18 | background-color: #fff; 19 | max-width: 1000px; 20 | } 21 | 22 | 23 | label { 24 | font-size: 1.1em; 25 | display: block; 26 | color: #555; 27 | } 28 | 29 | .input-title { 30 | width: 100%; 31 | padding: 10px; 32 | margin-bottom: 20px; 33 | font-size: 3.8em; 34 | border: none; 35 | border-radius: 5px; 36 | box-sizing: border-box; 37 | text-align: center; 38 | } 39 | 40 | .input-title:focus { 41 | outline: none; 42 | border: 1px solid #4899f8; /* 聚焦时的边框颜色 */ 43 | } 44 | 45 | .editable { 46 | padding: 15px; 47 | background-color: #fff; 48 | font-size: 1.1em; 49 | line-height: 1.6; 50 | border-radius: 5px; 51 | overflow-y: auto; 52 | min-height: 200px; 53 | } 54 | 55 | 56 | .right-section { 57 | flex: 1; 58 | padding: 20px; 59 | background-color: #f9f9f9; 60 | border-radius: 10px; 61 | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); 62 | margin-top: 35px; 63 | margin-right: 29px; 64 | } 65 | 66 | .platforms { 67 | display: flex; 68 | flex-wrap: wrap; 69 | gap: 15px; 70 | margin-bottom: 20px; 71 | justify-content: space-evenly; 72 | } 73 | 74 | .platform-item { 75 | display: flex; 76 | align-items: center; 77 | padding: 10px; 78 | background-color: #fff; 79 | border: 1px solid #ddd; 80 | border-radius: 5px; 81 | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); 82 | transition: background-color 0.3s ease, box-shadow 0.3s ease; 83 | cursor: pointer; 84 | } 85 | 86 | .platform-item:hover { 87 | background-color: #f1f1f1; 88 | } 89 | 90 | .platform-label { 91 | font-size: 1.1em; 92 | margin-left: 10px; 93 | color: #333; 94 | } 95 | 96 | .publish-btn { 97 | background-color: #4899f8; /* 主色调 */ 98 | color: white; 99 | padding: 10px 20px; 100 | border: none; 101 | border-radius: 5px; 102 | cursor: pointer; 103 | font-size: 1.1em; 104 | width: 100%; 105 | text-align: center; 106 | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); 107 | transition: background-color 0.3s ease, box-shadow 0.3s ease; 108 | margin-bottom: 20px; 109 | } 110 | 111 | .publish-btn:hover { 112 | background-color: #357bb5; /* 悬停时的颜色变化 */ 113 | box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3); 114 | } 115 | 116 | /* 响应式布局 */ 117 | @media (max-width: 768px) { 118 | .container { 119 | flex-direction: column; 120 | } 121 | 122 | .left-section, 123 | .right-section { 124 | border: none; 125 | } 126 | 127 | .platform-item { 128 | width: 100%; 129 | } 130 | } 131 | 132 | .hidden { 133 | display: none !important; 134 | } 135 | 136 | .loading-layer { 137 | position: fixed; 138 | top: 0; 139 | left: 0; 140 | width: 100vw; 141 | height: 100vh; 142 | background-color: rgba(0, 0, 0, 0.6); 143 | display: flex; 144 | justify-content: center; 145 | align-items: center; 146 | z-index: 9999; 147 | } 148 | 149 | .loading-content { 150 | text-align: -webkit-center; 151 | color: white; 152 | } 153 | 154 | .spinner { 155 | border: 5px solid rgba(255, 255, 255, 0.2); 156 | border-top: 5px solid #4899f8; 157 | border-radius: 50%; 158 | width: 40px; 159 | height: 40px; 160 | animation: spin 1s linear infinite; 161 | margin-bottom: 10px; 162 | } 163 | 164 | @keyframes spin { 165 | 100% { 166 | transform: rotate(360deg); 167 | } 168 | } 169 | 170 | #loadingText { 171 | font-size: 1.2em; 172 | } 173 | 174 | .content{ 175 | background-color: #f84866; 176 | } 177 | -------------------------------------------------------------------------------- /src/adapters/ZhiHuAdapter.js: -------------------------------------------------------------------------------- 1 | import BaseAdapter from '../core/BaseAdapter'; 2 | 3 | export default class ZhiHuAdapter extends BaseAdapter { 4 | constructor() { 5 | super(); 6 | this.version = '1.0'; 7 | this.type = 'zhihu'; 8 | this.name = '知乎'; 9 | } 10 | 11 | async getMetaData() { 12 | let res; 13 | try { 14 | res = await $.ajax({ 15 | url: 'https://www.zhihu.com/api/v4/me?include=account_status,is_bind_phone,is_force_renamed,email,renamed_fullname', 16 | }); 17 | if (res.error) { 18 | throw new Error('未登录'); 19 | } 20 | } catch (e) { 21 | throw new Error('未登录'); 22 | } 23 | 24 | return { 25 | uid: res.uid, 26 | title: res.name, 27 | avatar: res.avatar_url, 28 | type: 'zhihu', 29 | displayName: '知乎', 30 | home: 'https://www.zhihu.com/settings/account', 31 | icon: 'https://static.zhihu.com/static/favicon.ico', 32 | }; 33 | } 34 | 35 | async addPost(post) { 36 | await this.getMetaData() 37 | const res = await $.ajax({ 38 | url: 'https://zhuanlan.zhihu.com/api/articles/drafts', 39 | type: 'POST', 40 | dataType: 'JSON', 41 | contentType: 'application/json', 42 | data: JSON.stringify({ 43 | content: post.post_content, 44 | title: post.post_title, 45 | topics: 'MoeJue' 46 | }), 47 | }); 48 | this.publishPost(res.id) 49 | return { 50 | status: 'success', 51 | post_id: res.id, 52 | }; 53 | } 54 | 55 | async editPost(post, post_id) { 56 | const res = await $.ajax({ 57 | url: `https://zhuanlan.zhihu.com/api/articles/${post_id}/draft`, 58 | type: 'PATCH', 59 | contentType: 'application/json', 60 | data: JSON.stringify({ 61 | title: post.post_title, 62 | content: post.post_content, 63 | isTitleImageFullScreen: false, 64 | table_of_contents: false, 65 | delta_time: 10, 66 | // titleImage: `https://pic1.zhimg.com/${post.post_thumbnail}.png`, 67 | }), 68 | }); 69 | this.publishPost(post_id) 70 | return { 71 | status: 'success', 72 | post_id: post_id 73 | }; 74 | } 75 | 76 | // 目前是存草稿,现在需要把它设置为发布 77 | async publishPost(post_id) { 78 | await $.ajax({ 79 | url: `https://zhuanlan.zhihu.com/api/articles/${post_id}/publish`, 80 | type: 'PUT', 81 | dataType: 'JSON', 82 | contentType: 'application/json', 83 | data: JSON.stringify({ 84 | disclaimer_type: "none", 85 | disclaimer_status: "close", 86 | table_of_contents_enabled: false, 87 | commercial_report_info: { commercial_types: [] }, 88 | commercial_zhitask_bind_info: null, 89 | }), 90 | }); 91 | } 92 | 93 | async uploadFile(file) { 94 | const res = await $.ajax({ 95 | url: 'https://zhuanlan.zhihu.com/api/uploaded_images', 96 | type: 'POST', 97 | headers: { 98 | accept: '*/*', 99 | 'x-requested-with': 'fetch', 100 | }, 101 | data: { 102 | url: file.src, 103 | source: 'article', 104 | }, 105 | }); 106 | return [{ 107 | id: res.hash, 108 | object_key: res.hash, 109 | url: res.src, 110 | }]; 111 | } 112 | } -------------------------------------------------------------------------------- /src/options/options.js: -------------------------------------------------------------------------------- 1 | document.addEventListener('DOMContentLoaded', function () { 2 | getoPtionData(); 3 | loadSettings(); 4 | 5 | const saveButton = document.getElementById('save-settings'); 6 | if (saveButton) { 7 | saveButton.addEventListener('click', saveSettings); 8 | } else { 9 | console.error('保存按钮未找到'); 10 | } 11 | 12 | const tabButtons = document.querySelectorAll('.tab-button'); 13 | const tabContents = document.querySelectorAll('.tab-content'); 14 | 15 | tabButtons.forEach(button => { 16 | button.addEventListener('click', function () { 17 | tabButtons.forEach(btn => btn.classList.remove('active')); 18 | this.classList.add('active'); 19 | const tab = this.getAttribute('data-tab'); 20 | tabContents.forEach(content => { 21 | content.classList.remove('active'); 22 | if (content.id === tab) { 23 | content.classList.add('active'); 24 | } 25 | }); 26 | }); 27 | }); 28 | }); 29 | 30 | async function getoPtionData() { 31 | const platformSelect = document.getElementById('platform-select'); 32 | chrome.storage.local.get(['accounts'], (result) => { 33 | const accounts = result.accounts || []; 34 | if (accounts.length > 0) { 35 | accounts.forEach(account => { 36 | if (account.platform === "discuz") { 37 | const option = document.createElement('option'); 38 | option.value = account.url; 39 | option.textContent = `${account.platformName}`; 40 | platformSelect.appendChild(option); 41 | } 42 | }); 43 | } else { 44 | const option = document.createElement('option'); 45 | option.value = ""; 46 | option.textContent = "暂无可用平台"; 47 | platformSelect.appendChild(option); 48 | } 49 | }); 50 | } 51 | let data = {}; 52 | function loadSettings() { 53 | chrome.storage.local.get([ 54 | 'selectedPlatform', 55 | 'syncType', 56 | 'promoteToggle', 57 | 'data' 58 | ], function (result) { 59 | data = result; 60 | const platformSelect = document.getElementById('platform-select'); 61 | const selectedPlatform = result.selectedPlatform || ''; 62 | platformSelect.value = selectedPlatform; 63 | document.getElementById('forum-id').value = result.data[selectedPlatform].discuzForumId || ''; 64 | document.getElementById('category-id').value = result.data[selectedPlatform].discuzCategoryId || ''; 65 | document.getElementById('sync-type').value = result.syncType || '1'; 66 | document.getElementById('promote-toggle').checked = result.promoteToggle; 67 | }); 68 | } 69 | 70 | 71 | function saveSettings() { 72 | const selectedPlatform = document.getElementById('platform-select').value; 73 | const forumId = document.getElementById('forum-id').value; 74 | const categoryId = document.getElementById('category-id').value; 75 | const syncType = document.getElementById('sync-type').value; 76 | const promoteToggle = document.getElementById('promote-toggle').checked; 77 | const dataToSave = { 78 | syncType, 79 | promoteToggle, 80 | selectedPlatform, 81 | data: data.data || {} 82 | }; 83 | dataToSave.data[selectedPlatform] = { 84 | discuzForumId: forumId, 85 | discuzCategoryId: categoryId 86 | } 87 | chrome.storage.local.set(dataToSave, function () { 88 | document.getElementById('save-settings').innerText = '成功保存'; 89 | setTimeout(() => { 90 | document.getElementById('save-settings').innerText = '保存设置'; 91 | }, 2000); 92 | }); 93 | } 94 | 95 | document.getElementById('platform-select').addEventListener('change', function () { 96 | document.getElementById('forum-id').value = data.data[this.value] && data.data[this.value].discuzForumId || ''; 97 | document.getElementById('category-id').value = data.data[this.value] && data.data[this.value].discuzCategoryId || '' 98 | }); -------------------------------------------------------------------------------- /src/adapters/CnblogAdapter.js: -------------------------------------------------------------------------------- 1 | import BaseAdapter from '../core/BaseAdapter'; 2 | import { getCsrfToken } from '../core/getCsrfToken'; 3 | 4 | export default class CnblogAdapter extends BaseAdapter { 5 | constructor() { 6 | super(); 7 | this.version = '1.0'; 8 | this.type = 'cnblog'; 9 | this.name = '博客园'; 10 | chrome.storage.local.get(['syncType'], (result)=> { 11 | this.postType = result.syncType || 1; 12 | }); 13 | } 14 | 15 | async getMetaData() { 16 | var res = await $.ajax({ 17 | url: 'https://account.cnblogs.com/user/userinfo', 18 | method: 'GET', 19 | headers: { 20 | 'Accept': 'application/json, text/javascript' 21 | }, 22 | contentType: 'application/json', 23 | }); 24 | if (!res.displayName) { 25 | throw new Error('未登录') 26 | } 27 | return { 28 | uid: res.displayName, 29 | title: res.displayName, 30 | avatar: 'https:'+res.iconName, 31 | type: 'cnblog', 32 | displayName: '博客园', 33 | home: 'https://i.cnblogs.com/EditArticles.aspx?IsDraft=1', 34 | icon: 'https://i.cnblogs.com/favicon.ico', 35 | } 36 | } 37 | 38 | 39 | async addPost(post, post_id = null) { 40 | $.ajaxSetup({ 41 | headers: { 42 | 'Origin': 'https://i.cnblogs.com', 43 | 'Referer': 'https://i.cnblogs.com/', 44 | 'X-XSRF-TOKEN': await getCsrfToken('i.cnblogs.com', 'XSRF-TOKEN') 45 | } 46 | }); 47 | try { 48 | var res = await $.ajax({ 49 | url: 'https://i.cnblogs.com/api/posts', 50 | type: 'POST', 51 | data: JSON.stringify({ 52 | "id": post_id, 53 | "postType": parseInt(this.postType), //1是随笔,2是文章,3是日志 54 | "accessPermission": 0, 55 | "title": post.post_title, 56 | "url": null, 57 | "postBody": post.post_content, 58 | "categoryIds": null, 59 | "inSiteCandidate": false, 60 | "inSiteHome": false, 61 | "siteCategoryId": null, 62 | "blogTeamIds": null, 63 | "isPublished": true, 64 | "displayOnHomePage": true, //是否在首页显示 65 | "isAllowComments": true, 66 | "includeInMainSyndication": true, 67 | "isPinned": false, 68 | "isOnlyForRegisterUser": false, 69 | "isUpdateDateAdded": false, 70 | "entryName": null, 71 | "description": null, 72 | "tags": null, 73 | "password": null, 74 | "datePublished": new Date().toISOString(), 75 | "isMarkdown": true, 76 | "isDraft": true, // 是否是草稿 77 | "autoDesc": null, 78 | "changePostType": false, 79 | "blogId": 0, 80 | "author": null, 81 | "removeScript": false, 82 | "clientInfo": null, 83 | "changeCreatedTime": false, 84 | "canChangeCreatedTime": false 85 | }), 86 | contentType: 'application/json', 87 | success: function (data) { 88 | console.log('文章已保存为草稿:', data); 89 | }, 90 | error: function (error) { 91 | console.error('文章保存失败:', error); 92 | } 93 | }); 94 | } catch (error) { 95 | console.error('请求发生错误:', error); 96 | } 97 | return { 98 | status: 'success', 99 | post_id: res.id, 100 | } 101 | } 102 | 103 | async editPost(post, post_id) { 104 | this.addPost(post, post_id); 105 | return { 106 | status: 'success' 107 | } 108 | } 109 | 110 | 111 | async uploadFile(file) { 112 | return { 113 | url: file.src, 114 | } 115 | } 116 | 117 | 118 | 119 | 120 | } 121 | 122 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ArticleSync - 多平台文章同步插件 2 | 3 | ArticleSync 是一个浏览器扩展,帮助用户轻松将文章同步发布到多个社交平台。支持将文章从本地草稿发布到各大平台,如知乎、Bilibili 等。它提供了一站式解决方案,让你在不同的社交媒体平台上同步文章变得简单高效。 4 | 5 | 基于浏览器插件模式,自动检测本地登录账号,杜绝账号泄露,环境异常等风险 6 | 7 | 基于 chrome Manifest v3 浏览器扩展标准开发,注意内核版本要求 8 | 9 | 10 | ## 背景 11 | 你也知道,我这又一下子多了好几个博客平台,和一大堆社交网站,如果我想让他们之间都能保持活跃的更新怎么办.(证明我还活着) ~~还能一键盗文章~~ 12 | 13 | 我最常更新的就是我自己的小破站了,但是其他平台,我可能就只是偶尔更新一下,但是又不想每次都去手动发布,所以我就想,能不能写一个插件,自动检测我本地登录的账号,然后自动发布呢. 14 | 15 | 正所谓,自己动手丰衣足食.鼓捣了好几天.勉强算是能用的样子,剩下的就有空在更新了.~~除非你给我钱~~ 16 | 17 | 插件还有很多不完善的地方,我也没有多平台正式在生产环境中实测,如遇报错,实属正常,那就提交issue吧,或者自己改改,改好了再提交PR吧.嘻嘻~ 18 | 19 | 为了不影响我说话,截图放最后了 20 | 21 | 还有,开源不易,来个star吧,嘿嘿嘿~ 22 | 23 | ~~本来想加一点私货进去的,自动关注我的社区平台~~ 24 | 25 | ## 功能特色 26 | - **多平台支持**:支持知乎、Bilibili等各大主流平台,支持自建开源CMS系统。 27 | - **状态跟踪**:在插件界面中查看文章的同步状态. 28 | - **账号管理**:可查看与插件绑定的各平台账号信息。 29 | - **可扩展性强**:支持开发者通过适配器模式轻松扩展到更多平台。 30 | - **安全可靠**:插件基于浏览器扩展模式,确保账号安全,避免账号泄露等风险。 31 | 32 | ## Todo List 33 | - [ ] 独立文章编辑器 34 | - [ ] 图片一键同步 35 | - [x] markdown与HTML互转 36 | - [ ] 第三方图床系统 37 | - [ ] 多账号管理 38 | - [ ] 多系统客户端版本 39 | - [ ] 一键ai总结 40 | - [ ] 视频同步 41 | - [ ] 标签,分类的支持 42 | - [ ] 更加友好的错误处理 43 | - [ ] 更多平台的接入 44 | 45 | ## 支持渠道 46 | | 媒体 | 媒体行业 | 状态 | 网址 | 支持类型 | 更新时间 | 47 | |--------------|-------|-----|-----------------------------------|---------------|-----------| 48 | | 哔哩哔哩 | 主流自媒体 | 已支持 | https://bilibili.com/ | HTML | 2024/10/13 | 49 | | 知乎 | 主流自媒体 | 已支持 | https://www.zhihu.com/ | HTML | 2024/10/13 | 50 | | 博客园 | 博客 | 已支持 | https://cnblogs.com/ | HTML | 2024/10/14 | 51 | | 新浪头条 | 主流自媒体 | 已支持 | https://weibo.com/ | HTML | 2024/10/14 | 52 | | emlog | 开源CMS | 已支持 | https://www.emlog.net/ | HTML | 2024/10/14 | 53 | | WordPress | 开源CMS | 已支持 | https://cn.wordpress.org/ | HTML,Markdown | 2024/10/14 | 54 | | Discuz | 开源CMS | 已支持 | https://www.discuz.vip/ | Markdown,Text | 2024/10/15 | 55 | 56 | ## 安装说明 57 | 58 | 1. 克隆仓库到本地: 59 | ```bash 60 | git clone https://github.com/iAJue/Articlesync.git 61 | ``` 62 | 63 | 2. 进入项目目录: 64 | ```bash 65 | cd articlesync 66 | ``` 67 | 68 | 3. 安装依赖: 69 | ```bash 70 | npm install 71 | ``` 72 | 73 | 4. 打包项目 74 | ```bash 75 | npm run build 76 | ``` 77 | 78 | 5. 加载插件: 79 | - 打开 Chrome 浏览器,进入 chrome://extensions/。 80 | - 启用 开发者模式。 81 | - 点击 加载已解压的扩展程序,选择 dist/ 文件夹。 82 | 83 | 6. 开发 84 | 1. 启动开发环境 85 | ``` bash 86 | npm run watch-reload 87 | ``` 88 | 2. 以配置热更新,每次修改代码后,插件将自动打包,并且 Chrome 会自动重新加载插件。 89 | 90 | ## 如何添加一个适配器 91 | 1. 在 `src/adapters` 目录下创建一个新的适配器文件,例如 `PlatformAdapter.js`。 92 | 2. 继承 `BaseAdapter` 类,并实现以下方法: 93 | - `getMetaData()`: 获取当前页面的元数据。 94 | - `addPost(post)`: 添加新的文章。 95 | - `editPost(post, post_id)`: 编辑文章。 96 | - `uploadFile(file)`: 上传文件。 97 | - 定义`constructor`构造函数,设置适配器的版本、类型和名称或其他初始化数据. 98 | ``` 99 | constructor() { 100 | super(); 101 | this.version = '1.0'; 102 | this.type = 'Twitter'; 103 | this.name = '推特'; 104 | } 105 | ``` 106 | 3. 在 `src/adapters/adapters.js` 中导入并注册新的适配器。 107 | 108 | 109 | ## 项目结构 110 | ``` 111 | ├── src 112 | │ ├── adapters # 各平台的适配器 113 | │ │ ├── ZhiHuAdapter.js 114 | │ │ ├── BilibiliAdapter.js 115 | │ ├── contents # 内容脚本 116 | │ ├── background.js # 后台脚本 117 | │ ├── popup # 插件弹窗界面 118 | │ │ ├── popup.js 119 | │ │ ├── popup.html 120 | │ ├── options # 扩展选项页面 121 | │ │ ├── options.js 122 | │ │ ├── options.html 123 | │ ├── dist # 打包后的文件 124 | │ ├── manifest.json # Chrome 插件清单文件 125 | ├── webpack.config.js # Webpack 配置文件 126 | ├── package.json # 项目配置文件 127 | ├── README.md # 项目说明文件 128 | ├── .gitignore # Git 忽略文件 129 | 130 | ``` 131 | 132 | ## 贡献指南 133 | 134 | 欢迎对项目进行贡献!如果你有任何改进意见或想要添加新的平台支持,请遵循以下步骤: 135 | 136 | 1. Fork 仓库。 137 | 2. 创建一个新的分支。 138 | 3. 提交你的更改。 139 | 4. 发起一个 Pull Request。 140 | 141 | ## 反馈 142 | 如果你在使用过程中遇到任何问题或建议,请通过以下方式告诉我们: 143 | 144 | - 提交 [Issue](https://github.com/iAJue/Articlesync/issues) 145 | - BUG 146 | - 浏览器版本: Chrome 129.0.6668.90 147 | - 内核版本: 129.0.6668.90 148 | - 操作系统: Windows 10 149 | - 插件版本: 1.0.0 150 | - 复现步骤: 151 | - 错误描述: 152 | - 建议 153 | - 描述: 154 | - 期望效果: 155 | - 支持 156 | - 平台: 157 | - 网址: 158 | - 账号: (有最好) 159 | - Blog:访问 [阿珏酱のBlog](https://MoeJue.cn) 留言 160 | 161 | 162 | ## 投喂 ☕ 163 | 我很可爱,请给我钱! 164 | I am cute, please give me money! 165 | 166 | ![image](./images/Feeding.gif) 167 | 168 | #### 啥?没钱?没事,我也支持虚拟币 169 | 钱包地址:`0x56949baed7b69b09a1c5539230ba6ffadd0323c3` 170 | 171 | ## 许可证 172 | 173 | Copyright (c) 2024-present, iAJue 174 | 175 | 本项目遵循 [GPL-3.0](https://opensource.org/licenses/GPL-3.0) 许可证。 176 | 177 | ## 截图 178 | ![ArticleSync](./images/QQ20241016-162808.png) 179 | ![ArticleSync](./images/QQ20241016-162303.png) 180 | ![ArticleSync](./images/QQ20241016-162333.png) 181 | ![ArticleSync](./images/QQ20241016-162937.png) 182 | ![ArticleSync](./images/QQ20241016-163214.png) -------------------------------------------------------------------------------- /src/adapters/BilibiliAdapter.js: -------------------------------------------------------------------------------- 1 | import BaseAdapter from '../core/BaseAdapter'; 2 | import { getCsrfToken } from '../core/getCsrfToken'; 3 | 4 | export default class BilibiliAdapter extends BaseAdapter { 5 | constructor() { 6 | super(); 7 | this.version = '1.0'; 8 | this.type = 'bilibili'; 9 | this.name = '哔哩哔哩'; 10 | } 11 | 12 | async getMetaData() { 13 | var res = await $.ajax({ 14 | url: 'https://api.bilibili.com/x/web-interface/nav?build=0&mobi_app=web', 15 | }) 16 | if (!res.data.isLogin) { 17 | throw new Error('未登录') 18 | } 19 | return { 20 | uid: res.data.mid, 21 | title: res.data.uname, 22 | avatar: res.data.face, 23 | type: 'bilibili', 24 | displayName: '哔哩哔哩', 25 | home: 'https://member.bilibili.com/platform/upload/text', 26 | icon: 'https://www.bilibili.com/favicon.ico', 27 | } 28 | } 29 | 30 | async addPost(post) { 31 | var res = await this.editPost(post); //没有打草稿不允许发表 32 | await this.editPost(post, res.post_id); 33 | return { 34 | status: 'success', 35 | post_id: 0, 36 | } 37 | } 38 | 39 | async editPost(post, post_id = 0) { 40 | var url = 'https://api.bilibili.com/x/article/creative/draft/addupdate'; //草稿箱地址 41 | var post_data = { 42 | "title": post.post_title, 43 | "content": post.post_content, 44 | "category": 0,//专栏分类,0为默认 45 | "list_id": 0,//文集编号,默认0不添加到文集 46 | "tid": 4, //4为专栏封面单图,3为专栏封面三图 47 | "reprint": 0, 48 | "media_id": 0, 49 | "spoiler": 0, 50 | "original": 1, 51 | "csrf": await getCsrfToken('bilibili.com', 'bili_jct') 52 | }; 53 | if (post_id){ 54 | post_data["aid"] = post_id; 55 | url = 'https://api.bilibili.com/x/article/creative/article/submit'; //正式发表地址 56 | } 57 | 58 | var res = await $.ajax({ 59 | url: url, 60 | type: 'POST', 61 | dataType: 'JSON', 62 | data:post_data 63 | }) 64 | if (!res.data) { 65 | throw new Error(res.message) 66 | } 67 | return { 68 | status: 'success', 69 | post_id: res.data.aid 70 | } 71 | } 72 | 73 | async uploadFile(file) { 74 | var src = file.src 75 | var csrf = this.config.state.csrf 76 | 77 | var uploadUrl = 'https://api.bilibili.com/x/article/creative/article/upcover' 78 | var file = new File([file.bits], 'temp', { 79 | type: file.type, 80 | }) 81 | var formdata = new FormData() 82 | formdata.append('binary', file) 83 | formdata.append('csrf', csrf) 84 | var res = await axios({ 85 | url: uploadUrl, 86 | method: 'post', 87 | data: formdata, 88 | headers: { 'Content-Type': 'multipart/form-data' }, 89 | }) 90 | 91 | if (res.data.code != 0) { 92 | throw new Error('图片上传失败 ' + src) 93 | } 94 | console.log('uploadFile', res) 95 | var id = Math.floor(Math.random() * 100000) 96 | return [ 97 | { 98 | id: id, 99 | object_key: id, 100 | url: res.data.data.url, 101 | size: res.data.data.size, 102 | // images: [res.data], 103 | }, 104 | ] 105 | } 106 | 107 | async preEditPost(post) { 108 | var div = $('
') 109 | $('body').append(div) 110 | 111 | div.html(post.content) 112 | var doc = div 113 | var pres = doc.find('a') 114 | for (let mindex = 0; mindex < pres.length; mindex++) { 115 | const pre = pres.eq(mindex) 116 | try { 117 | pre.after(pre.html()).remove() 118 | } catch (e) { } 119 | } 120 | 121 | tools.processDocCode(div) 122 | tools.makeImgVisible(div) 123 | 124 | var pres = doc.find('iframe') 125 | for (let mindex = 0; mindex < pres.length; mindex++) { 126 | const pre = pres.eq(mindex) 127 | try { 128 | pre.remove() 129 | } catch (e) { } 130 | } 131 | 132 | try { 133 | const images = doc.find('img') 134 | for (let index = 0; index < images.length; index++) { 135 | const image = images.eq(index) 136 | const imgSrc = image.attr('src') 137 | if (imgSrc && imgSrc.indexOf('.svg') > -1) { 138 | console.log('remove svg Image') 139 | image.remove() 140 | } 141 | } 142 | const qqm = doc.find('qqmusic') 143 | qqm.next().remove() 144 | qqm.remove() 145 | } catch (e) { } 146 | 147 | post.content = $('
') 148 | .append(doc.clone()) 149 | .html() 150 | console.log('post', post) 151 | } 152 | 153 | editImg(img, source) { 154 | img.attr('size', source.size) 155 | } 156 | 157 | } 158 | -------------------------------------------------------------------------------- /src/sync/sync.js: -------------------------------------------------------------------------------- 1 | import syncManager from '../core/syncManager'; 2 | import { extractArticle, htmlToMarkdown, htmlToText, markdownToHtml } from '../contents/extractArticle'; 3 | 4 | document.addEventListener('DOMContentLoaded', async () => { 5 | const platformsContainer = document.getElementById('platforms'); 6 | const supportedPlatforms = await syncManager.getSupportedPlatforms(); 7 | if (supportedPlatforms && supportedPlatforms.length > 0) { 8 | supportedPlatforms.forEach(platform => { 9 | const platformItem = document.createElement('div'); 10 | platformItem.classList.add('platform-item'); 11 | 12 | const checkbox = document.createElement('input'); 13 | checkbox.type = 'checkbox'; 14 | checkbox.id = platform.value; 15 | checkbox.name = 'platform'; 16 | checkbox.value = platform.value; 17 | checkbox.classList.add('checkbox'); 18 | 19 | const label = document.createElement('label'); 20 | label.setAttribute('for', platform.value); 21 | label.classList.add('platform-label'); 22 | label.textContent = platform.label; 23 | 24 | platformItem.appendChild(checkbox); 25 | platformItem.appendChild(label); 26 | platformsContainer.appendChild(platformItem); 27 | }); 28 | } 29 | 30 | // 从存储中加载文章数据 31 | chrome.storage.local.get(['article'], (result) => { 32 | if (result.article) { 33 | document.getElementById('title').value = result.article.title || "无标题"; 34 | chrome.storage.local.get(['promoteToggle'], (res) => { 35 | let content = result.article.content || "无内容"; 36 | if (res.promoteToggle != false ) { 37 | content = addPromotion(content) 38 | } 39 | document.getElementById('content').innerHTML = content; 40 | }); 41 | } else { 42 | alert('没有提取到文章内容'); 43 | } 44 | }); 45 | }); 46 | 47 | 48 | document.addEventListener('DOMContentLoaded', async () => { 49 | document.getElementById('publishButton').addEventListener('click', async () => { 50 | const title = document.getElementById('title').value; 51 | const content = document.getElementById('content').innerHTML; 52 | const selectedPlatforms = Array.from(document.querySelectorAll('input[name="platform"]:checked')).map(el => { 53 | const platformValue = el.value; 54 | const platformLabel = document.querySelector(`label[for="${el.id}"]`).textContent; 55 | return { 56 | value: platformValue, 57 | label: platformLabel 58 | }; 59 | }); 60 | 61 | const loadingLayer = document.getElementById('loadingLayer'); 62 | const loadingText = document.getElementById('loadingText'); 63 | loadingLayer.classList.remove('hidden'); 64 | 65 | const statusUpdates = []; 66 | for (const platform of selectedPlatforms) { 67 | try { 68 | loadingText.textContent = `正在同步到 ${platform.label}...`; 69 | await syncManager.syncPost(platform.value, { post_title: title, post_content: content }); 70 | statusUpdates.push({ 71 | platform: platform.label, 72 | status: 'success', 73 | message: `成功`, 74 | title: title 75 | }); 76 | } catch (error) { 77 | statusUpdates.push({ 78 | platform: platform.label, 79 | status: 'failed', 80 | message: error.message, 81 | title: title 82 | }); 83 | } 84 | } 85 | chrome.storage.local.set({ syncStatus: statusUpdates }, () => { 86 | // window.close() 87 | loadingText.textContent = '同步任务结束!'; 88 | loadingLayer.classList.add('hidden'); 89 | alert('同步完成!') 90 | }); 91 | }); 92 | }); 93 | 94 | 95 | function addPromotion(content) { 96 | const params = new URLSearchParams(window.location.search); 97 | const currentUrl = params.get('currentUrl'); 98 | var sharcode = `

本文使用 文章同步助手 同步.原文地址: ${currentUrl}

` 99 | return content.trim() + `${sharcode}` 100 | } 101 | 102 | // 获取文章内容 103 | function getArticleContent() { 104 | const content = document.getElementById("content").innerHTML; 105 | return content; 106 | } 107 | 108 | // 1. HTML 转 Markdown 109 | function handleHtmlToMarkdown() { 110 | const htmlContent = getArticleContent(); 111 | const markdownContent = htmlToMarkdown(htmlContent); 112 | document.getElementById("content").innerHTML = markdownContent; 113 | } 114 | 115 | // 2. HTML 转 纯文本 116 | function handleHtmlToText() { 117 | const htmlContent = getArticleContent(); 118 | const textContent = htmlToText(htmlContent); 119 | document.getElementById("content").innerText = textContent; 120 | } 121 | 122 | // 3. Markdown 转 HTML 123 | function handleMarkdownToHtml() { 124 | const markdownContent = getArticleContent(); 125 | const htmlContent = markdownToHtml(markdownContent); 126 | document.getElementById("content").innerHTML = htmlContent; 127 | } 128 | 129 | // 事件绑定到按钮 130 | document.getElementById("htmlToMarkdown").addEventListener("click", handleHtmlToMarkdown); 131 | document.getElementById("htmlToText").addEventListener("click", handleHtmlToText); 132 | document.getElementById("markdownToHtml").addEventListener("click", handleMarkdownToHtml); -------------------------------------------------------------------------------- /src/adapters/EmlogAdapter.js: -------------------------------------------------------------------------------- 1 | import BaseAdapter from '../core/BaseAdapter'; 2 | 3 | export default class EmlogAdapter extends BaseAdapter { 4 | constructor() { 5 | super(); 6 | this.version = '1.2.0'; 7 | this.type = 'emlog'; 8 | this.name = 'emlog'; 9 | this.url = {}; 10 | this.token = {}; 11 | } 12 | 13 | async getMetaData() { 14 | return new Promise((resolve, reject) => { 15 | chrome.storage.local.get(['accounts'], async (result) => { 16 | const accounts = result.accounts || []; 17 | 18 | const emlogAccounts = accounts.filter(account => account.platform === this.type); 19 | if (emlogAccounts.length === 0) { 20 | return reject(new Error('未找到 emlog 数据或未保存 URL')); 21 | } 22 | 23 | const results = []; 24 | 25 | for (const emlogAccount of emlogAccounts) { 26 | const finalUrl = emlogAccount.url + '/admin/blogger.php'; 27 | try { 28 | const response = await $.ajax({ 29 | url: finalUrl, 30 | }); 31 | 32 | const parser = new DOMParser(); 33 | const doc = parser.parseFromString(response, 'text/html'); 34 | const avatarMatch = response.match(/]*src=['"](?:\.\.\/)?(content\/uploadfile\/[^'"]+)['"]/); 35 | const avatarUrl = avatarMatch ? emlogAccount.url + avatarMatch[1] : null; 36 | const usernameInput = doc.querySelector('input[name="username"]'); 37 | const username = usernameInput ? usernameInput.value : null; 38 | const tokenInput = doc.querySelector('input[name="token"]'); 39 | this.token[emlogAccount.platformName] = tokenInput ? tokenInput.value : null; 40 | 41 | if (!username) { 42 | throw new Error(`${emlogAccount.platformName} 未检测到登录信息`); 43 | } 44 | 45 | this.url[emlogAccount.platformName] = emlogAccount.url; 46 | 47 | results.push({ 48 | uid: 1, 49 | title: username, 50 | avatar: avatarUrl, 51 | type: 'emlog', 52 | displayName: 'emlog', 53 | home: emlogAccount.url + '/admin/admin_log.php', 54 | icon: emlogAccount.url + '/favicon.ico', 55 | }); 56 | 57 | } catch (error) { 58 | console.error(`${emlogAccount.platformName} 处理时出错: ${error.message}`); 59 | continue; 60 | } 61 | } 62 | if (results.length > 0) { 63 | resolve(results); 64 | } else { 65 | reject(new Error('未能成功获取任何账户的登录信息')); 66 | } 67 | }); 68 | }); 69 | } 70 | 71 | async addPost(post) { 72 | await this.getMetaData(); 73 | const errors = []; 74 | 75 | for (const platformName in this.url) { 76 | try { 77 | const platformUrl = this.url[platformName]; 78 | const now = new Date(); 79 | const formattedDate = now.toISOString().slice(0, 19).replace('T', ' '); 80 | 81 | // 构建要发送的数据 82 | const formData = new FormData(); 83 | formData.append('title', post.post_title); 84 | formData.append('as_logid', '-1'); 85 | formData.append('content', post.post_content); 86 | formData.append('excerpt', ''); 87 | formData.append('sort', '-1'); 88 | formData.append('tag', ''); 89 | formData.append('postdate', formattedDate); 90 | formData.append('alias', ''); 91 | formData.append('password', ''); 92 | formData.append('allow_remark', 'y'); 93 | formData.append('token', this.token[platformName]); 94 | formData.append('ishide', ''); 95 | formData.append('gid', '-1'); 96 | formData.append('author', '1'); 97 | 98 | const response = await $.ajax({ 99 | url: `${platformUrl}/admin/save_log.php?action=add`, 100 | type: 'POST', 101 | processData: false, 102 | contentType: false, 103 | data: formData, 104 | }); 105 | 106 | // 这里可以添加复杂的判断,来确认是否发表成功 107 | console.log(`${platformName} 发表成功`); 108 | } catch (error) { 109 | errors.push(`${platformName} 发表失败: ${error.message}`); 110 | console.error(`${platformName} 发表失败: ${error.message}`); 111 | continue; // 出错时继续尝试下一个平台 112 | } 113 | } 114 | 115 | if (errors.length > 0) { 116 | throw new Error(JSON.stringify(errors)); 117 | } 118 | 119 | return { 120 | status: 'success' 121 | }; 122 | } 123 | 124 | async editPost(post, post_id) { 125 | 126 | return { 127 | status: 'success', 128 | post_id: post_id 129 | }; 130 | } 131 | 132 | 133 | async uploadFile(file) { 134 | 135 | return [{ 136 | id: res.hash, 137 | object_key: res.hash, 138 | url: res.src, 139 | }]; 140 | } 141 | } -------------------------------------------------------------------------------- /src/popup/popup.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: 'Arial', sans-serif; 3 | margin: 0; 4 | padding: 0; 5 | width: 425px; 6 | height: 500px; 7 | max-height: 700px; 8 | background-color: #f0f4f8; 9 | } 10 | 11 | .container { 12 | display: flex; 13 | flex-direction: column; 14 | height: 100%; 15 | box-sizing: border-box; 16 | position: relative; 17 | overflow-y: auto; 18 | padding-bottom: 66.9px; 19 | background-color: #FFF; 20 | } 21 | 22 | .nav-tabs { 23 | display: flex; 24 | background-color: #ffffff; 25 | border-bottom: 2px solid #d1d1d1; 26 | } 27 | 28 | .tab { 29 | flex: 1; 30 | padding: 12px; 31 | text-align: center; 32 | cursor: pointer; 33 | font-size: 1.1em; 34 | color: #5c5c5c; 35 | transition: background-color 0.3s ease, color 0.3s ease; 36 | } 37 | 38 | .tab.active { 39 | background-color: #ffffff; 40 | border-bottom: 2px solid #4899f8; 41 | color: #333; 42 | } 43 | 44 | .content { 45 | flex: 1; 46 | padding: 20px; 47 | overflow-y: auto; 48 | background-color: #ffffff; 49 | } 50 | 51 | .hidden { 52 | display: none !important; 53 | } 54 | 55 | .content-section h3 { 56 | font-size: 1.5em; 57 | margin-bottom: 20px; 58 | color: #4899f8; 59 | } 60 | 61 | 62 | .task-item span { 63 | font-size: 14px; 64 | } 65 | 66 | .task-status { 67 | padding: 0px 8px; 68 | border-radius: 5px; 69 | display: flex; 70 | justify-content: center; 71 | align-items: center; 72 | width: 40px; 73 | } 74 | 75 | .success { 76 | background-color: #4899f8; 77 | color: white; 78 | } 79 | 80 | .pending { 81 | background-color: #ff9800; 82 | color: white; 83 | } 84 | 85 | .failed { 86 | background-color: #f44336; 87 | color: white; 88 | } 89 | 90 | .bottom-buttons { 91 | display: flex; 92 | justify-content: space-between; 93 | background-color: #ffffff; 94 | border-top: 1px solid #d1d1d1; 95 | position: absolute; 96 | bottom: 5px; 97 | width: 100%; 98 | padding-top: 6px; 99 | } 100 | 101 | .btn { 102 | padding: 11px 16px; 103 | background-color: #4899f8; 104 | border: none; 105 | border-radius: 5px; 106 | color: white; 107 | cursor: pointer; 108 | transition: background-color 0.3s ease; 109 | margin: 6px; 110 | } 111 | 112 | .btn:hover { 113 | background-color: #357bb5; 114 | } 115 | 116 | #add-account-section { 117 | padding: 20px; 118 | background: #FFF; 119 | height: 100%; 120 | } 121 | 122 | #other-account-list { 123 | margin-top: 10px; 124 | } 125 | 126 | .account-options { 127 | display: flex; 128 | flex-wrap: wrap; 129 | gap: 15px; 130 | justify-content: center; 131 | } 132 | 133 | .account-option { 134 | display: flex; 135 | flex-direction: column; 136 | align-items: center; 137 | justify-content: center; 138 | width: 45%; 139 | height: 120px; 140 | background-color: #ffffff; 141 | border: 2px solid #ddd; 142 | border-radius: 10px; 143 | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); 144 | transition: border-color 0.3s ease, box-shadow 0.3s ease; 145 | cursor: pointer; 146 | } 147 | 148 | .account-option:hover { 149 | border-color: #4899f8; 150 | box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); 151 | } 152 | 153 | .account-option.selected { 154 | border-color: #4899f8; 155 | box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); 156 | } 157 | 158 | .platform-icon { 159 | width: 40px; 160 | height: 40px; 161 | margin-bottom: 10px; 162 | } 163 | 164 | .platform-name { 165 | font-size: 1.1em; 166 | color: #555; 167 | } 168 | 169 | #back-to-tabs-btn { 170 | padding: 4px 20px; 171 | background-color: #4899f8; 172 | border: none; 173 | border-radius: 5px; 174 | color: white; 175 | cursor: pointer; 176 | transition: background-color 0.3s ease; 177 | } 178 | 179 | #back-to-tabs-btn:hover { 180 | background-color: #357bb5; 181 | } 182 | 183 | #account-list { 184 | border-radius: 5px; 185 | display: flex; 186 | flex-wrap: wrap; 187 | justify-content: center; 188 | gap: 10px; 189 | } 190 | 191 | #account-list .task-item { 192 | display: flex; 193 | padding: 10px; 194 | background-color: #ffffff; 195 | border: 1px solid #ddd; 196 | border-radius: 5px; 197 | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); 198 | transition: border-color 0.3s ease, box-shadow 0.3s ease; 199 | width: 90px; 200 | flex-wrap: wrap; 201 | flex-direction: column; 202 | justify-content: space-evenly; 203 | align-items: center; 204 | } 205 | 206 | #account-list p { 207 | color: #555; 208 | font-size: 1em; 209 | margin: 0; 210 | } 211 | 212 | input[type="text"] { 213 | width: 67%; 214 | padding: 8px 20px; 215 | margin: 6px 0; 216 | display: inline-block; 217 | border: 2px solid #ccc; 218 | border-radius: 6px; 219 | box-sizing: border-box; 220 | font-size: 14px; 221 | background-color: #f9f9f9; 222 | transition: all 0.3s ease; 223 | } 224 | 225 | input[type="text"]:focus { 226 | border-color: #4899f8; 227 | box-shadow: 0 0 10px rgba(72, 153, 248, 0.5); 228 | outline: none; 229 | background-color: #fff; 230 | } 231 | 232 | input[type="text"]::placeholder { 233 | color: #aaa; 234 | font-style: italic; 235 | } 236 | 237 | input[type="text"]:not(:placeholder-shown) { 238 | border-color: #28a745; 239 | } 240 | 241 | .account-url-input { 242 | display: flex; 243 | flex-direction: column; 244 | margin-top: 6px; 245 | } 246 | 247 | #save-account-btn { 248 | position: absolute; 249 | right: 30px; 250 | margin-top: -38px; 251 | height: 78px; 252 | background-color: #4899f8; 253 | } 254 | 255 | #save-account-btn:hover { 256 | background-color: #357bb5; 257 | } 258 | 259 | .delete-btn { 260 | background: #ffc400; 261 | color: #FFF; 262 | border: 0px solid; 263 | border-radius: 5px; 264 | cursor: pointer; 265 | } 266 | 267 | .delete-btn:hover { 268 | background-color: #ffb800; 269 | } 270 | 271 | 272 | .task-items { 273 | display: flex; 274 | justify-content: space-between; 275 | padding: 6px; 276 | margin-bottom: 10px; 277 | border: 1px solid #ddd; 278 | border-radius: 5px; 279 | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); 280 | transition: border-color 0.3s ease, box-shadow 0.3s ease; 281 | font-size: 15px; 282 | } 283 | 284 | .task-title{ 285 | width: 300px; 286 | } -------------------------------------------------------------------------------- /src/adapters/DiscuzAdapter.js: -------------------------------------------------------------------------------- 1 | import BaseAdapter from '../core/BaseAdapter'; 2 | 3 | export default class DiscuzAdapter extends BaseAdapter { 4 | constructor() { 5 | super(); 6 | this.version = '1.2.0'; 7 | this.type = 'discuz'; 8 | this.name = 'discuz'; 9 | this.url = {}; 10 | this.formhash = {}; 11 | chrome.storage.local.get(['data'], (result) => { 12 | this.data = result.data 13 | }); 14 | } 15 | 16 | async getMetaData() { 17 | return new Promise((resolve, reject) => { 18 | chrome.storage.local.get(['accounts'], async (result) => { 19 | const accounts = result.accounts || []; 20 | 21 | const discuzAccounts = accounts.filter(account => account.platform === this.type); 22 | if (discuzAccounts.length === 0) { 23 | return reject(new Error('未找到 discuz 数据或未保存 URL')); 24 | } 25 | 26 | const results = []; 27 | 28 | for (const discuzAccount of discuzAccounts) { 29 | const finalUrl = discuzAccount.url + '/home.php?mod=spacecp&ac=profile'; 30 | try { 31 | const response = await $.ajax({ 32 | url: finalUrl, 33 | }); 34 | 35 | const parser = new DOMParser(); 36 | const doc = parser.parseFromString(response, 'text/html'); 37 | const usernameElement = doc.querySelector('strong.vwmy.qq a'); 38 | const username = usernameElement ? usernameElement.textContent.trim() : null; 39 | const avatarElement = doc.querySelector('div.avt img'); 40 | const avatarUrl = avatarElement ? avatarElement.src : null; 41 | const tokenInput = doc.querySelector('input[name="token"]'); 42 | this.token = tokenInput ? tokenInput.value : null; 43 | const uidMatch = response.match(/discuz_uid\s*=\s*'(\d+)'/); 44 | const uid = uidMatch ? uidMatch[1] : null; 45 | const formhashInput = doc.querySelector('input[name="formhash"]'); 46 | this.formhash[discuzAccount.platformName] = formhashInput ? formhashInput.value : null; 47 | if (!username || !uid) { 48 | throw new Error(`${discuzAccount.platformName} 未检测到登录信息`); 49 | } 50 | 51 | this.url[discuzAccount.platformName] = discuzAccount.url; 52 | 53 | results.push({ 54 | uid: uid, 55 | title: username, 56 | avatar: avatarUrl, 57 | type: 'discuz', 58 | displayName: 'discuz', 59 | home: discuzAccount.url, 60 | icon: discuzAccount.url + '/favicon.ico', 61 | }); 62 | 63 | } catch (error) { 64 | console.error(`${discuzAccount.platformName} 处理时出错: ${error.message}`); 65 | continue; 66 | } 67 | } 68 | if (results.length > 0) { 69 | resolve(results); 70 | } else { 71 | reject(new Error('未能成功获取任何账户的登录信息')); 72 | } 73 | }); 74 | }); 75 | } 76 | 77 | async addPost(post) { 78 | await this.getMetaData(); 79 | const errors = []; 80 | for (const platformName in this.url) { 81 | try { 82 | const platformUrl = this.url[platformName]; 83 | 84 | const now = Math.floor(Date.now() / 1000); 85 | if (!this.data[platformUrl].discuzForumId || !this.data[platformUrl].discuzCategoryId) { 86 | throw new Error('板块ID或分类ID未设置'); 87 | } 88 | 89 | // 构建要发送的数据 90 | const data = { 91 | formhash: this.formhash[platformName], 92 | posttime: now, 93 | wysiwyg: '1', 94 | typeid: this.data[platformUrl].discuzCategoryId, 95 | subject: post.post_title, 96 | message: `[md]${post.post_content}[/md]`, 97 | replycredit_times: '1', 98 | replycredit_extcredits: '0', 99 | replycredit_membertimes: '1', 100 | replycredit_random: '100', 101 | readperm: '', 102 | price: '', 103 | tags: '', 104 | cronpublishdate: '', 105 | ordertype: '1', 106 | allownoticeauthor: '1', 107 | usesig: '1', 108 | save: '', 109 | file: '', 110 | file: '' 111 | }; 112 | const encodedData = $.param(data); 113 | const response = await $.ajax({ 114 | url: `${platformUrl}/forum.php?mod=post&action=newthread&fid=${this.data[platformUrl].discuzForumId}&extra=&topicsubmit=yes`, 115 | type: 'POST', 116 | data: encodedData, 117 | contentType: 'application/x-www-form-urlencoded', 118 | }); 119 | // 这里可以做更加复杂的判断,判断是否真正发表成功 120 | console.log(`${platformName} 发表成功`); 121 | } catch (error) { 122 | errors.push(`${platformName} 发表失败: ${error.message}`); 123 | console.error(`${platformName} 发表失败: ${error.message}`); 124 | continue; 125 | } 126 | } 127 | if (errors.length > 0) { 128 | throw new Error(JSON.stringify(errors)); 129 | } 130 | return { 131 | status: 'success' 132 | }; 133 | } 134 | 135 | async editPost(post, post_id) { 136 | 137 | return { 138 | status: 'success', 139 | post_id: post_id 140 | }; 141 | } 142 | 143 | 144 | async uploadFile(file) { 145 | 146 | return [{ 147 | id: res.hash, 148 | object_key: res.hash, 149 | url: res.src, 150 | }]; 151 | } 152 | } -------------------------------------------------------------------------------- /src/adapters/WeiboAdapter.js: -------------------------------------------------------------------------------- 1 | import BaseAdapter from '../core/BaseAdapter'; 2 | 3 | export default class WeiboAdapter extends BaseAdapter { 4 | constructor() { 5 | super(); 6 | this.version = '1.0'; 7 | this.type = 'weibo'; 8 | this.name = '微博'; 9 | this.uid = null; 10 | } 11 | 12 | async getMetaData() { 13 | try { 14 | const html = await $.get('https://card.weibo.com/article/v3/editor'); 15 | const uidMatch = html.match(/\$CONFIG\['uid'\]\s*=\s*(\d+)/); 16 | const nickMatch = html.match(/\$CONFIG\['nick'\]\s*=\s*'([^']+)'/); 17 | const avatarMatch = html.match(/\$CONFIG\['avatar_large'\]\s*=\s*'([^']+)'/); 18 | 19 | if (uidMatch && nickMatch && avatarMatch) { 20 | const uid = uidMatch[1]; 21 | const nick = nickMatch[1]; 22 | const avatar = avatarMatch[1]; 23 | this.uid = uid; 24 | return { 25 | uid: uid, 26 | title: nick, 27 | avatar: 'https://image.baidu.com/search/down?url='+avatar, 28 | displayName: '微博', 29 | type: 'weibo', 30 | home: 'https://card.weibo.com/article/v3/editor', 31 | icon: 'https://weibo.com/favicon.ico', 32 | }; 33 | } else { 34 | throw new Error('CONFIG not found'); 35 | } 36 | } catch (error) { 37 | console.error('Error fetching Weibo user metadata:', error); 38 | throw error; 39 | } 40 | } 41 | 42 | async addPost(post) { 43 | await this.getMetaData(); 44 | var res = await $.post( 45 | 'https://card.weibo.com/article/v3/aj/editor/draft/create?uid=' + 46 | this.uid 47 | ) 48 | if (res.code != 100000) { 49 | throw new Error(res.msg) 50 | } 51 | 52 | console.log(res) 53 | var post_id = res.data.id 54 | var post_data = { 55 | id: post_id, 56 | title: post.post_title, 57 | subtitle: '', 58 | type: '', 59 | status: '0', 60 | publish_at: '', 61 | error_msg: '', 62 | error_code: '0', 63 | collection: '[]', 64 | free_content: '', 65 | content: post.post_content, 66 | cover: '', 67 | summary: '', 68 | writer: '', 69 | extra: 'null', 70 | is_word: '0', 71 | article_recommend: '[]', 72 | follow_to_read: '0', //仅粉丝阅读全文 73 | isreward: '1', 74 | pay_setting: '{"ispay":0,"isvclub":0}', 75 | source: '0', 76 | action: '1', 77 | content_type: '0', 78 | save: '1', 79 | } 80 | 81 | var res = await $.ajax({ 82 | url: 83 | 'https://card.weibo.com/article/v3/aj/editor/draft/save?uid=' + 84 | this.uid + 85 | '&id=' + 86 | post_id, 87 | type: 'POST', 88 | dataType: 'JSON', 89 | headers: { 90 | accept: 'application/json', 91 | }, 92 | data: post_data, 93 | }) 94 | var res = await $.ajax({ 95 | url: 96 | 'https://card.weibo.com/article/v3/aj/editor/draft/publish?uid=' + 97 | this.uid + 98 | '&id=' + 99 | post_id, 100 | type: 'POST', 101 | dataType: 'JSON', 102 | headers: { 103 | accept: 'application/json', 104 | }, 105 | data: post_data, 106 | }) 107 | 108 | console.log(res) 109 | return { 110 | status: 'success', 111 | post_id: post_id, 112 | } 113 | } 114 | 115 | async editPost(post_id, post) { 116 | var res = await $.ajax({ 117 | url: 118 | 'https://card.weibo.com/article/v3/aj/editor/draft/save?uid=' + 119 | this.uid + 120 | '&id=' + 121 | post_id, 122 | type: 'POST', 123 | dataType: 'JSON', 124 | headers: { 125 | accept: 'application/json', 126 | }, 127 | data: { 128 | id: post_id, 129 | title: post.post_title, 130 | subtitle: '', 131 | type: '', 132 | status: '0', 133 | publish_at: '', 134 | error_msg: '', 135 | error_code: '0', 136 | collection: '[]', 137 | free_content: '', 138 | content: post.post_content, 139 | cover: post.post_thumbnail_raw ? post.post_thumbnail_raw.url : '', 140 | summary: '', 141 | writer: '', 142 | extra: 'null', 143 | is_word: '0', 144 | article_recommend: '[]', 145 | follow_to_read: '0', 146 | isreward: '1', 147 | pay_setting: '{"ispay":0,"isvclub":0}', 148 | source: '0', 149 | action: '1', 150 | content_type: '0', 151 | save: '1', 152 | }, 153 | }) 154 | 155 | if (res.code == '111006') { 156 | throw new Error(res.msg) 157 | } 158 | console.log(res) 159 | return { 160 | status: 'success', 161 | post_id: post_id, 162 | } 163 | } 164 | 165 | untiImageDone(src) { 166 | return new Promise((resolve, reject) => { 167 | ; (async function loop() { 168 | var res = await $.ajax({ 169 | url: 170 | 'https://card.weibo.com/article/v3/aj/editor/plugins/asyncimginfo?uid=' + 171 | this.uid, 172 | type: 'POST', 173 | headers: { 174 | accept: '*/*', 175 | 'x-requested-with': 'fetch', 176 | }, 177 | data: { 178 | 'urls[0]': src, 179 | }, 180 | }) 181 | 182 | var done = res.data[0].task_status_code == 1 183 | if (done) { 184 | resolve(res.data[0]) 185 | } else { 186 | setTimeout(loop, 1000) 187 | } 188 | })() 189 | }) 190 | } 191 | 192 | async uploadFileByUrl(file) { 193 | var src = file.src 194 | var res = await $.ajax({ 195 | url: 196 | 'https://card.weibo.com/article/v3/aj/editor/plugins/asyncuploadimg?uid=' + 197 | this.uid, 198 | type: 'POST', 199 | headers: { 200 | accept: '*/*', 201 | 'x-requested-with': 'fetch', 202 | }, 203 | data: { 204 | 'urls[0]': src, 205 | }, 206 | }) 207 | 208 | var imgDetail = await this.untiImageDone(src) 209 | return [ 210 | { 211 | id: imgDetail.pid, 212 | object_key: imgDetail.pid, 213 | url: imgDetail.url, 214 | }, 215 | ] 216 | } 217 | 218 | async uploadFile(file) { 219 | var blob = new Blob([file.bits]) 220 | console.log('uploadFile', file, blob) 221 | var uploadurl1 = `https://picupload.weibo.com/interface/pic_upload.php?app=miniblog&s=json&p=1&data=1&url=&markpos=1&logo=0&nick=&file_source=4` 222 | var uploadurl2 = 'https://picupload.weibo.com/interface/pic_upload.php?app=miniblog&s=json&p=1&data=1&url=&markpos=1&logo=0&nick=' 223 | var fileResp = await $.ajax({ 224 | url: 225 | uploadurl1, 226 | type: 'POST', 227 | processData: false, 228 | data: new Blob([file.bits]), 229 | }) 230 | console.log(file, fileResp) 231 | return [ 232 | { 233 | id: fileResp.data.pics.pic_1.pid, 234 | object_key: fileResp.data.pics.pic_1.pid, 235 | url: 236 | 'https://wx3.sinaimg.cn/large/' + 237 | fileResp.data.pics.pic_1.pid + 238 | '.jpg', 239 | }, 240 | ] 241 | } 242 | 243 | } 244 | -------------------------------------------------------------------------------- /src/popup/popup.js: -------------------------------------------------------------------------------- 1 | import adapters from '../adapters/adapters'; 2 | 3 | document.addEventListener('DOMContentLoaded', function () { 4 | loadAccounts(); 5 | loadSyncStatus(); 6 | }); 7 | 8 | document.getElementById('status-tab').addEventListener('click', function () { 9 | switchTab('status'); 10 | }); 11 | document.getElementById('account-tab').addEventListener('click', function () { 12 | switchTab('account'); 13 | }); 14 | document.getElementById('about-tab').addEventListener('click', function () { 15 | switchTab('about'); 16 | }); 17 | 18 | function switchTab(tab) { 19 | document.querySelectorAll('.tab').forEach(tab => tab.classList.remove('active')); 20 | document.querySelectorAll('.content-section').forEach(section => section.classList.add('hidden')); 21 | document.getElementById(`${tab}-tab`).classList.add('active'); 22 | document.getElementById(`${tab}-content`).classList.remove('hidden'); 23 | } 24 | 25 | // 加载同步状态并显示在状态区域 26 | function loadSyncStatus() { 27 | chrome.storage.local.get(['syncStatus'], (result) => { 28 | const statusContainer = document.getElementById('task-status'); 29 | statusContainer.innerHTML = ''; 30 | 31 | const syncStatus = result.syncStatus || []; 32 | 33 | if (syncStatus.length > 0) { 34 | syncStatus.forEach((task) => { 35 | console.log(task) 36 | const taskItem = document.createElement('div'); 37 | taskItem.classList.add('task-items'); 38 | const taskTitle = document.createElement('span'); 39 | taskTitle.classList.add('task-title'); 40 | if(task.status === 'success'){ 41 | taskTitle.textContent = `${task.platform}: ${task.title}`; 42 | }else{ 43 | taskTitle.textContent = `${task.platform}: ${task.title} - ${task.message}`; 44 | } 45 | const taskStatus = document.createElement('span'); 46 | taskStatus.classList.add('task-status'); 47 | taskStatus.textContent = task.status === 'success' ? '成功' : '失败'; 48 | taskStatus.classList.add(task.status); 49 | taskItem.appendChild(taskTitle); 50 | taskItem.appendChild(taskStatus); 51 | statusContainer.appendChild(taskItem); 52 | }); 53 | } else { 54 | statusContainer.innerHTML = '

暂无同步任务!

'; 55 | } 56 | }); 57 | } 58 | 59 | // 加载账号信息 60 | async function loadAccounts() { 61 | const accountList = document.getElementById('account-list'); 62 | accountList.innerHTML = ''; 63 | 64 | for (const adapter of adapters) { 65 | try { 66 | const accountData = await adapter.getMetaData(); 67 | const accountDataArray = Array.isArray(accountData) ? accountData : [accountData]; 68 | for (const account of accountDataArray) { 69 | const accountItem = document.createElement('div'); 70 | accountItem.classList.add('task-item'); 71 | const platformIcon = document.createElement('img'); 72 | platformIcon.src = account.icon; 73 | platformIcon.alt = account.displayName; 74 | platformIcon.style.width = '24px'; 75 | const avatarIcon = document.createElement('img'); 76 | avatarIcon.src = account.avatar; 77 | avatarIcon.style.width = '24px'; 78 | avatarIcon.style.borderRadius = '50%'; 79 | const accountName = document.createElement('span'); 80 | accountName.textContent = account.title; 81 | const accountDisplayName = document.createElement('span'); 82 | accountDisplayName.textContent = account.displayName; 83 | 84 | accountItem.appendChild(platformIcon); 85 | accountItem.appendChild(accountDisplayName); 86 | accountItem.appendChild(avatarIcon); 87 | accountItem.appendChild(accountName); 88 | accountList.appendChild(accountItem); 89 | } 90 | } catch (error) { 91 | console.error(`加载 ${adapter.name} 账号信息失败:`, error); 92 | } 93 | } 94 | } 95 | 96 | document.getElementById('group-btn').addEventListener('click', () => { 97 | chrome.tabs.create({ url: 'https://jq.qq.com/?_wv=1027&k=5cvR0GN' }); 98 | }) 99 | document.getElementById('feedback-btn').addEventListener('click', () => { 100 | chrome.tabs.create({ url: 'https://github.com/iAJue/Articlesync/issues' }); 101 | }) 102 | document.getElementById('clear-btn').addEventListener('click', () => { 103 | chrome.storage.local.clear(); 104 | document.getElementById('task-status').innerHTML = '

暂无同步任务!

'; 105 | }) 106 | 107 | document.getElementById('add-account-btn').addEventListener('click', () => { 108 | document.querySelector('.nav-tabs').classList.add('hidden'); 109 | document.querySelector('.content').classList.add('hidden'); 110 | document.querySelector('.bottom-buttons').classList.add('hidden'); 111 | document.getElementById('add-account-section').classList.remove('hidden'); 112 | loadSavedAccounts(); 113 | }); 114 | document.getElementById('back-to-tabs-btn').addEventListener('click', () => { 115 | document.querySelector('.nav-tabs').classList.remove('hidden'); 116 | document.querySelector('.content').classList.remove('hidden'); 117 | document.querySelector('.bottom-buttons').classList.remove('hidden'); 118 | document.getElementById('add-account-section').classList.add('hidden'); 119 | }); 120 | 121 | document.querySelectorAll('.account-option').forEach(option => { 122 | option.addEventListener('click', (event) => { 123 | document.querySelectorAll('.account-option').forEach(item => { 124 | item.classList.remove('selected'); 125 | }); 126 | const selectedOption = event.currentTarget; 127 | selectedOption.classList.add('selected'); 128 | const platform = selectedOption.getAttribute('data-platform'); 129 | document.getElementById('account-url-input').classList.remove('hidden'); 130 | document.getElementById('save-account-btn').setAttribute('data-platform', platform); 131 | }); 132 | }); 133 | 134 | document.getElementById('save-account-btn').addEventListener('click', () => { 135 | const url = document.getElementById('account-url').value; 136 | const platformName = document.getElementById('account-name').value; 137 | const platform = document.getElementById('save-account-btn').getAttribute('data-platform'); 138 | 139 | if(platform == 'Typecho'){ 140 | alert('暂不支持Typecho平台'); 141 | return; 142 | } 143 | 144 | if (url && platformName) { 145 | chrome.storage.local.get(['accounts'], (result) => { 146 | const accounts = result.accounts || []; 147 | const existingAccount = accounts.find(account => account.url === url); 148 | if (existingAccount) { 149 | alert('此网址已经存在,无法重复添加。'); 150 | } else { 151 | accounts.push({ platform, platformName, url }); 152 | chrome.storage.local.set({ accounts }, () => { 153 | console.log('账号已保存:', platform, platformName, url); 154 | document.getElementById('account-url-input').classList.add('hidden'); 155 | document.getElementById('account-url').value = ''; 156 | document.getElementById('account-name').value = ''; 157 | document.getElementById('add-account-list').classList.remove('hidden'); 158 | 159 | loadSavedAccounts(); 160 | }); 161 | } 162 | }); 163 | } else { 164 | alert('请输入有效的网址和平台名称。'); 165 | } 166 | }); 167 | 168 | 169 | 170 | function deleteAccount(index) { 171 | chrome.storage.local.get(['accounts'], (result) => { 172 | const accounts = result.accounts || []; 173 | accounts.splice(index, 1); 174 | chrome.storage.local.set({ accounts }, () => { 175 | loadSavedAccounts(); 176 | }); 177 | }); 178 | } 179 | 180 | function loadSavedAccounts() { 181 | chrome.storage.local.get(['accounts'], (result) => { 182 | const accountList = document.getElementById('other-account-list'); 183 | accountList.innerHTML = ''; 184 | 185 | const accounts = result.accounts || []; 186 | 187 | if (accounts.length > 0) { 188 | accounts.forEach((account, index) => { 189 | const accountItem = document.createElement('div'); 190 | accountItem.classList.add('task-items'); 191 | const platformName = document.createElement('span'); 192 | platformName.textContent = `${account.platform}: ${account.platformName} - ${account.url}`; 193 | accountItem.appendChild(platformName); 194 | const deleteButton = document.createElement('button'); 195 | deleteButton.textContent = '删除'; 196 | deleteButton.classList.add('delete-btn'); 197 | deleteButton.addEventListener('click', () => { 198 | deleteAccount(index); 199 | }); 200 | 201 | accountItem.appendChild(deleteButton); 202 | accountList.appendChild(accountItem); 203 | }); 204 | } 205 | }); 206 | } -------------------------------------------------------------------------------- /src/adapters/WordPressAdapter.js: -------------------------------------------------------------------------------- 1 | import BaseAdapter from '../core/BaseAdapter'; 2 | 3 | export default class WordPressAdapter extends BaseAdapter { 4 | constructor() { 5 | super(); 6 | this.version = '1.2.0'; 7 | this.type = 'wordpress'; 8 | this.name = 'WordPress'; 9 | this.url = {}; 10 | this.token = {}; 11 | } 12 | 13 | async getMetaData() { 14 | return new Promise((resolve, reject) => { 15 | chrome.storage.local.get(['accounts'], async (result) => { 16 | const accounts = result.accounts || []; 17 | 18 | const wordpressAccounts = accounts.filter(account => account.platform === this.type); 19 | if (wordpressAccounts.length === 0) { 20 | return reject(new Error('未找到 WordPress 数据或未保存 URL')); 21 | } 22 | 23 | const results = []; 24 | 25 | for (const wordpressAccount of wordpressAccounts) { 26 | const finalUrl = wordpressAccount.url + 'wp-admin'; 27 | try { 28 | const response = await $.ajax({ 29 | url: finalUrl, 30 | xhrFields: { 31 | withCredentials: true, 32 | }, 33 | }); 34 | 35 | const parser = new DOMParser(); 36 | const doc = parser.parseFromString(response, 'text/html'); 37 | 38 | const avatarElement = doc.querySelector('.ab-item img.avatar'); 39 | const avatarUrl = avatarElement ? avatarElement.src : null; 40 | const displayNameElement = doc.querySelector('.ab-item .display-name'); 41 | const displayName = displayNameElement ? displayNameElement.textContent.trim() : null; 42 | 43 | const tokenInput = doc.querySelector('input[name="token"]'); 44 | this.token[wordpressAccount.platformName] = tokenInput ? tokenInput.value : null; 45 | 46 | if (!displayName) { 47 | throw new Error(`${wordpressAccount.platformName} 未检测到登录信息`); 48 | } 49 | 50 | this.url[wordpressAccount.platformName] = wordpressAccount.url; 51 | 52 | results.push({ 53 | uid: 1, 54 | title: displayName, 55 | avatar: avatarUrl, 56 | type: 'WordPress', 57 | displayName: 'WordPress', 58 | home: wordpressAccount.url + '/wp-admin', 59 | icon: wordpressAccount.url + '/favicon.ico', 60 | }); 61 | 62 | } catch (error) { 63 | console.error(`${wordpressAccount.platformName} 处理时出错: ${error.message}`); 64 | continue; 65 | } 66 | } 67 | 68 | if (results.length > 0) { 69 | resolve(results); 70 | } else { 71 | reject(new Error('未能成功获取任何账户的登录信息')); 72 | } 73 | }); 74 | }); 75 | } 76 | 77 | async addPost(post) { 78 | await this.getMetaData(); 79 | const errors = []; 80 | 81 | for (const platformName in this.url) { 82 | try { 83 | const platformUrl = this.url[platformName]; 84 | 85 | const res = await $.ajax({ 86 | url: `${platformUrl}wp-admin/post-new.php`, 87 | method: 'POST', 88 | }); 89 | 90 | const parser = new DOMParser(); 91 | const doc = parser.parseFromString(res, 'text/html'); 92 | const _wpnonce = doc.querySelector('#_wpnonce').value; 93 | const _wp_http_referer = doc.querySelector('input[name="_wp_http_referer"]').value; 94 | const user_ID = doc.querySelector('#user-id').value; 95 | const action = doc.querySelector('#hiddenaction').value; 96 | const originalaction = doc.querySelector('#originalaction').value; 97 | const post_author = doc.querySelector('#post_author').value; 98 | const post_type = doc.querySelector('#post_type').value; 99 | const original_post_status = doc.querySelector('#original_post_status').value; 100 | const referredby = doc.querySelector('#referredby').value; 101 | const _wp_original_http_referer = doc.querySelector('input[name="_wp_original_http_referer"]').value; 102 | const post_ID = doc.querySelector('#post_ID').value; 103 | const meta_box_order_nonce = doc.querySelector('#meta-box-order-nonce').value; 104 | const closedpostboxesnonce = doc.querySelector('#closedpostboxesnonce').value; 105 | const samplepermalinknonce = doc.querySelector('#samplepermalinknonce').value; 106 | const _ajax_noncey = doc.querySelector('#_ajax_nonce-add-category').value; 107 | const _ajax_nonce = doc.querySelector('#_ajax_nonce-add-meta').value; 108 | 109 | const now = new Date(); 110 | const year = now.getFullYear(); 111 | const month = String(now.getMonth() + 1).padStart(2, '0'); 112 | const day = String(now.getDate()).padStart(2, '0'); 113 | const hours = String(now.getHours()).padStart(2, '0'); 114 | const minutes = String(now.getMinutes()).padStart(2, '0'); 115 | const seconds = String(now.getSeconds()).padStart(2, '0'); 116 | 117 | const data = { 118 | _wpnonce: _wpnonce, 119 | _wp_http_referer: _wp_http_referer, 120 | user_ID: user_ID, 121 | action: action, 122 | originalaction: originalaction, 123 | post_author: post_author, 124 | post_type: post_type, 125 | original_post_status: original_post_status, 126 | referredby: referredby, 127 | _wp_original_http_referer: _wp_original_http_referer, 128 | post_ID: post_ID, 129 | 'meta-box-order-nonce': meta_box_order_nonce, 130 | closedpostboxesnonce: closedpostboxesnonce, 131 | post_title: post.post_title, 132 | samplepermalinknonce: samplepermalinknonce, 133 | content: post.post_content, 134 | 'wp-preview': '', 135 | hidden_post_status: 'draft', 136 | post_status: 'draft', 137 | hidden_post_password: '', 138 | hidden_post_visibility: 'public', 139 | visibility: 'public', 140 | post_password: '', 141 | aa: year, 142 | mm: month, 143 | jj: day, 144 | hh: hours, 145 | mn: minutes, 146 | ss: seconds, 147 | hidden_mm: month, 148 | cur_mm: month, 149 | hidden_jj: day, 150 | cur_jj: day, 151 | hidden_aa: year, 152 | cur_aa: year, 153 | hidden_hh: hours, 154 | cur_hh: hours, 155 | hidden_mn: minutes, 156 | cur_mn: minutes, 157 | original_publish: '发布', 158 | publish: '发布', 159 | post_format: '0', 160 | 'post_category[]': '0', 161 | newcategory: '新分类名', 162 | newcategory_parent: '-1', 163 | '_ajax_nonce-add-category': _ajax_noncey, 164 | 'tax_input[post_tag]': '', 165 | 'newtag[post_tag]': '', 166 | _thumbnail_id: '-1', 167 | excerpt: '', 168 | trackback_url: '', 169 | metakeyselect: '#NONE#', 170 | metakeyinput: '', 171 | metavalue: '', 172 | '_ajax_nonce-add-meta': _ajax_nonce, 173 | advanced_view: '1', 174 | comment_status: 'open', 175 | ping_status: 'open', 176 | post_name: '', 177 | post_author_override: '1', 178 | }; 179 | 180 | const response = await $.ajax({ 181 | url: `${platformUrl}wp-admin/post.php`, 182 | type: 'POST', 183 | data: $.param(data), 184 | contentType: 'application/x-www-form-urlencoded', 185 | }); 186 | 187 | console.log(`${platformName} 发表成功`); 188 | } catch (error) { 189 | errors.push(`${platformName} 发表失败: ${error.message}`); 190 | console.error(`${platformName} 发表失败: ${error.message}`); 191 | continue; 192 | } 193 | } 194 | 195 | if (errors.length > 0) { 196 | throw new Error(JSON.stringify(errors)); 197 | } 198 | 199 | return { 200 | status: 'success' 201 | }; 202 | } 203 | 204 | async editPost(post, post_id) { 205 | 206 | return { 207 | status: 'success', 208 | post_id: post_id 209 | }; 210 | } 211 | 212 | 213 | async uploadFile(file) { 214 | 215 | return [{ 216 | id: res.hash, 217 | object_key: res.hash, 218 | url: res.src, 219 | }]; 220 | } 221 | } --------------------------------------------------------------------------------