Skip to content

Commit 8013f1a

Browse files
authored
72 feat support toutiao (#73)
* feat: add Toutiao platform support for dynamic content sync - Add localization for Toutiao platform in English and Chinese - Implement dynamic content synchronization for Toutiao platform - Register Toutiao platform in sync configuration - Create dedicated sync function for Toutiao dynamic content upload - Support text content and image uploads - Implement robust element selection and interaction with Toutiao's web interface * feat: add Toutiao article platform sync support - Implement article upload functionality for Toutiao platform - Add platform configuration for Toutiao article sync in common configuration - Create dedicated sync function for Toutiao article content upload - Support image processing, cover image handling, and content synchronization - Implement robust element selection and interaction with Toutiao's web interface
1 parent bfc88b3 commit 8013f1a

File tree

5 files changed

+294
-1
lines changed

5 files changed

+294
-1
lines changed

locales/en/messages.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -599,5 +599,9 @@
599599
"platformBaijiahao": {
600600
"message": "Baijiahao",
601601
"description": "Platform name for Baijiahao"
602+
},
603+
"platformToutiao": {
604+
"message": "Toutiao",
605+
"description": "Platform name for Toutiao"
602606
}
603607
}

locales/zh_CN/messages.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -595,5 +595,9 @@
595595
"platformBaijiahao": {
596596
"message": "百家号",
597597
"description": "百家号平台名称"
598+
},
599+
"platformToutiao": {
600+
"message": "今日头条",
601+
"description": "今日头条平台名称"
598602
}
599603
}

src/sync/article/toutiao.ts

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
import type { ArticleData, FileData, SyncData } from '~sync/common';
2+
3+
export async function ArticleToutiao(data: SyncData) {
4+
const articleData = data.data as ArticleData;
5+
console.log('message', data);
6+
function waitForElement(selector: string, timeout = 10000): Promise<Element> {
7+
return new Promise((resolve, reject) => {
8+
const element = document.querySelector(selector);
9+
if (element) {
10+
resolve(element);
11+
return;
12+
}
13+
14+
const observer = new MutationObserver(() => {
15+
const element = document.querySelector(selector);
16+
if (element) {
17+
resolve(element);
18+
observer.disconnect();
19+
}
20+
});
21+
22+
observer.observe(document.body, {
23+
childList: true,
24+
subtree: true,
25+
});
26+
27+
setTimeout(() => {
28+
observer.disconnect();
29+
reject(new Error(`Element with selector "${selector}" not found within ${timeout}ms`));
30+
}, timeout);
31+
});
32+
}
33+
34+
async function processContent(content: string): Promise<void> {
35+
await waitForElement('div[contenteditable="true"]');
36+
await new Promise((resolve) => setTimeout(resolve, 1000));
37+
38+
// 处理标题
39+
const titleTextarea = document.querySelector('textarea[placeholder="请输入文章标题(2~30个字)"]');
40+
if (titleTextarea) {
41+
(titleTextarea as HTMLTextAreaElement).value = articleData.title?.slice(0, 30) || '';
42+
titleTextarea.dispatchEvent(new Event('input', { bubbles: true }));
43+
titleTextarea.dispatchEvent(new Event('change', { bubbles: true }));
44+
}
45+
console.log('titleTextarea', titleTextarea);
46+
47+
// 处理内容
48+
const editor = document.querySelector('div[contenteditable="true"]') as HTMLElement;
49+
if (!editor) {
50+
console.log('未找到编辑器元素');
51+
return;
52+
}
53+
54+
editor.focus();
55+
const pasteEvent = new ClipboardEvent('paste', {
56+
bubbles: true,
57+
cancelable: true,
58+
clipboardData: new DataTransfer(),
59+
});
60+
pasteEvent.clipboardData.setData('text/html', content || '');
61+
editor.dispatchEvent(pasteEvent);
62+
editor.dispatchEvent(new Event('input', { bubbles: true }));
63+
editor.dispatchEvent(new Event('change', { bubbles: true }));
64+
65+
await new Promise((resolve) => setTimeout(resolve, 5000));
66+
}
67+
68+
async function processCover(coverData: FileData): Promise<void> {
69+
// 清除现有封面
70+
const clearExistingCovers = async () => {
71+
for (let i = 0; i < 20; i++) {
72+
const closeButton = document.querySelector('.article-cover-delete') as HTMLElement;
73+
if (!closeButton) break;
74+
console.log('Clicking close button', closeButton);
75+
closeButton.click();
76+
await new Promise((resolve) => setTimeout(resolve, 500));
77+
}
78+
};
79+
80+
await clearExistingCovers();
81+
82+
// 上传新封面
83+
const uploadButton = document.querySelector('div[class="article-cover-add"]');
84+
if (!uploadButton) return;
85+
86+
console.log('Found upload image button');
87+
uploadButton.dispatchEvent(new Event('click', { bubbles: true }));
88+
await new Promise((resolve) => setTimeout(resolve, 1000));
89+
90+
// 切换到上传图片标签
91+
const tabs = document.querySelectorAll('div.byte-tabs-header-title');
92+
const uploadTab = Array.from(tabs).find((tab) => tab.textContent?.includes('上传图片'));
93+
if (uploadTab) {
94+
uploadTab.dispatchEvent(new Event('click', { bubbles: true }));
95+
await new Promise((resolve) => setTimeout(resolve, 1000));
96+
}
97+
98+
// 上传文件
99+
const fileInput = document.querySelector('input[type="file"]');
100+
if (!fileInput) {
101+
console.log('未找到文件输入元素');
102+
return;
103+
}
104+
105+
const dataTransfer = new DataTransfer();
106+
console.log('try upload file', coverData);
107+
108+
const response = await fetch(coverData.url);
109+
const buffer = await response.arrayBuffer();
110+
const file = new File([buffer], coverData.name, { type: coverData.type });
111+
dataTransfer.items.add(file);
112+
113+
if (dataTransfer.files.length > 0) {
114+
(fileInput as HTMLInputElement).files = dataTransfer.files;
115+
fileInput.dispatchEvent(new Event('change', { bubbles: true }));
116+
fileInput.dispatchEvent(new Event('input', { bubbles: true }));
117+
}
118+
119+
await new Promise((resolve) => setTimeout(resolve, 5000));
120+
121+
// 确认上传
122+
const confirmButton = document.querySelector('button[data-e2e="imageUploadConfirm-btn"]');
123+
if (confirmButton) {
124+
console.log('Clicking confirm button for image upload');
125+
confirmButton.dispatchEvent(new Event('click', { bubbles: true }));
126+
await new Promise((resolve) => setTimeout(resolve, 2000));
127+
}
128+
}
129+
130+
// 主流程
131+
try {
132+
await processContent(articleData.content);
133+
134+
if (articleData.cover) {
135+
await processCover(articleData.cover);
136+
}
137+
138+
// 发布或预览
139+
const buttons = document.querySelectorAll('button.publish-btn');
140+
const publishButton = Array.from(buttons).find((btn) => btn.textContent?.includes('预览并发布'));
141+
142+
if (publishButton && data.auto_publish) {
143+
console.log('sendButton clicked');
144+
publishButton.dispatchEvent(new Event('click', { bubbles: true }));
145+
} else {
146+
console.log("未找到'发送'按钮");
147+
}
148+
} catch (error) {
149+
console.error('发布文章失败:', error);
150+
throw error;
151+
}
152+
}

src/sync/common.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ import { DynamicKuaishou } from './dynamic/kuaishou';
2626
import { DynamicBaijiahao } from './dynamic/baijiahao';
2727
import { VideoBaijiahao } from './video/baijiahao';
2828
import { ArticleBaijiahao } from './article/baijiahao';
29+
import { DynamicToutiao } from './dynamic/toutiao';
30+
import { ArticleToutiao } from './article/toutiao';
2931

3032
export interface SyncData {
3133
platforms: string[];
@@ -157,10 +159,18 @@ export const infoMap: Record<string, PlatformInfo> = {
157159
injectUrl: 'https://baijiahao.baidu.com/builder/rc/edit?type=news',
158160
injectFunction: ArticleBaijiahao,
159161
},
162+
ARTICLE_TOUTIAO: {
163+
type: 'ARTICLE',
164+
name: 'ARTICLE_TOUTIAO',
165+
homeUrl: 'https://mp.toutiao.com/',
166+
faviconUrl: 'https://sf1-cdn-tos.toutiaostatic.com/obj/ttfe/pgcfe/sz/mp_logo.png',
167+
platformName: chrome.i18n.getMessage('platformToutiao'),
168+
injectUrl: 'https://mp.toutiao.com/profile_v4/graphic/publish',
169+
injectFunction: ArticleToutiao,
170+
},
160171
DYNAMIC_X: {
161172
type: 'DYNAMIC',
162173
name: 'DYNAMIC_X',
163-
164174
homeUrl: 'https://x.com/home',
165175
faviconUrl: 'https://x.com/favicon.ico',
166176
iconifyIcon: 'simple-icons:x',
@@ -290,6 +300,15 @@ export const infoMap: Record<string, PlatformInfo> = {
290300
injectUrl: 'https://baijiahao.baidu.com/builder/rc/edit?type=events',
291301
injectFunction: DynamicBaijiahao,
292302
},
303+
DYNAMIC_TOUTIAO: {
304+
type: 'DYNAMIC',
305+
name: 'DYNAMIC_TOUTIAO',
306+
homeUrl: 'https://mp.toutiao.com/',
307+
faviconUrl: 'https://sf1-cdn-tos.toutiaostatic.com/obj/ttfe/pgcfe/sz/mp_logo.png',
308+
platformName: chrome.i18n.getMessage('platformToutiao'),
309+
injectUrl: 'https://mp.toutiao.com/profile_v4/weitoutiao/publish',
310+
injectFunction: DynamicToutiao,
311+
},
293312
VIDEO_BILIBILI: {
294313
type: 'VIDEO',
295314
name: 'VIDEO_BILIBILI',

src/sync/dynamic/toutiao.ts

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import { type DynamicData, type SyncData } from '../common';
2+
3+
// 不支持发布视频
4+
export async function DynamicToutiao(data: SyncData) {
5+
function waitForElement(selector: string, timeout = 10000): Promise<Element> {
6+
return new Promise((resolve, reject) => {
7+
const element = document.querySelector(selector);
8+
if (element) {
9+
resolve(element);
10+
return;
11+
}
12+
13+
const observer = new MutationObserver(() => {
14+
const element = document.querySelector(selector);
15+
if (element) {
16+
resolve(element);
17+
observer.disconnect();
18+
}
19+
});
20+
21+
observer.observe(document.body, {
22+
childList: true,
23+
subtree: true,
24+
});
25+
26+
setTimeout(() => {
27+
observer.disconnect();
28+
reject(new Error(`Element with selector "${selector}" not found within ${timeout}ms`));
29+
}, timeout);
30+
});
31+
}
32+
33+
try {
34+
const { content, images, title } = data.data as DynamicData;
35+
36+
// 等待编辑器出现
37+
const editor = (await waitForElement('div[contenteditable="true"]')) as HTMLElement;
38+
await new Promise((resolve) => setTimeout(resolve, 1000));
39+
40+
if (editor) {
41+
// 更新编辑器内容,将标题和内容合并
42+
const combinedContent = title ? `${title}\n\n${content || ''}` : content || '';
43+
editor.innerText = combinedContent;
44+
editor.focus();
45+
editor.dispatchEvent(new Event('input', { bubbles: true }));
46+
await new Promise((resolve) => setTimeout(resolve, 3000));
47+
}
48+
49+
// 清除已有图片
50+
const clearExistingImages = async () => {
51+
for (let i = 0; i < 20; i++) {
52+
const closeButton = document.querySelector('.image-remove-btn') as HTMLElement;
53+
if (!closeButton) break;
54+
closeButton.click();
55+
await new Promise((resolve) => setTimeout(resolve, 500));
56+
}
57+
};
58+
await clearExistingImages();
59+
60+
// 处理图片上传
61+
if (images?.length > 0) {
62+
const uploadButtons = document.querySelectorAll('button.syl-toolbar-button');
63+
const uploadButton = Array.from(uploadButtons).find((button) => button.textContent?.includes('图片'));
64+
65+
if (uploadButton) {
66+
uploadButton.dispatchEvent(new Event('click', { bubbles: true }));
67+
await new Promise((resolve) => setTimeout(resolve, 1000));
68+
69+
const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
70+
if (fileInput) {
71+
const dataTransfer = new DataTransfer();
72+
73+
for (const image of images) {
74+
if (!image.type.startsWith('image/')) {
75+
console.log('跳过非图片文件:', image);
76+
continue;
77+
}
78+
79+
const response = await fetch(image.url);
80+
const arrayBuffer = await response.arrayBuffer();
81+
const file = new File([arrayBuffer], image.name, { type: image.type });
82+
dataTransfer.items.add(file);
83+
}
84+
85+
if (dataTransfer.files.length > 0) {
86+
fileInput.files = dataTransfer.files;
87+
fileInput.dispatchEvent(new Event('change', { bubbles: true }));
88+
fileInput.dispatchEvent(new Event('input', { bubbles: true }));
89+
}
90+
91+
// 等待上传完成
92+
await new Promise((resolve) => setTimeout(resolve, 5000));
93+
94+
// 点击确认按钮
95+
const confirmButton = document.querySelector('button[data-e2e="imageUploadConfirm-btn"]');
96+
if (confirmButton) {
97+
confirmButton.dispatchEvent(new Event('click', { bubbles: true }));
98+
await new Promise((resolve) => setTimeout(resolve, 2000));
99+
}
100+
}
101+
}
102+
}
103+
104+
// 发布内容
105+
const publishButton = document.querySelector('button.publish-content') as HTMLButtonElement;
106+
if (publishButton) {
107+
if (data.auto_publish) {
108+
publishButton.dispatchEvent(new Event('click', { bubbles: true }));
109+
}
110+
}
111+
} catch (error) {
112+
console.error('头条发布过程中出错:', error);
113+
}
114+
}

0 commit comments

Comments
 (0)