├── 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 | 标签为换行符
34 | let textContent = htmlContent.replace(/ 除WordPress、Emlog等开源自建平台外,其它平台只要在当前浏览器登录过即可被识别到账号,无需手动添加 本文使用 文章同步助手 同步.原文地址: ${currentUrl} 暂无同步任务! 暂无同步任务!
/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 | "ArticleSync 是一个浏览器扩展,帮助用户轻松将文章同步发布到多个社交平台。它提供了一站式解决方案,让你在不同的社交媒体平台上同步文章变得简单高效。
51 | 插件版本:1.0.0
52 | 版权信息:© 2024 阿珏酱
53 | GitHub:https://github.com/iAJue/Articlesync
54 |
55 |
`
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 = '