Skip to content

Commit 9d3f1f5

Browse files
authored
62 feat kuaishou platform (#65)
* feat: add Kuaishou platform localization support * feat: add Kuaishou video platform sync support * feat: enhance Kuaishou video sync with improved file input and publish functionality * feat: add Kuaishou dynamic platform sync support
1 parent 6115a90 commit 9d3f1f5

File tree

5 files changed

+397
-0
lines changed

5 files changed

+397
-0
lines changed

locales/en/messages.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -591,5 +591,9 @@
591591
},
592592
"optionsEnterMarkdown": {
593593
"message": "Enter Markdown content..."
594+
},
595+
"platformKuaishou": {
596+
"message": "Kuaishou",
597+
"description": "Platform name for Kuaishou"
594598
}
595599
}

locales/zh_CN/messages.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -587,5 +587,9 @@
587587
},
588588
"optionsEnterMarkdown": {
589589
"message": "输入 Markdown 内容..."
590+
},
591+
"platformKuaishou": {
592+
"message": "快手",
593+
"description": "快手平台名称"
590594
}
591595
}

src/sync/common.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ import { ArticleJianshu } from './article/jianshu';
2121
import { ArticleSegmentfault } from './article/segmentfault';
2222
import { DynamicReddit } from './dynamic/reddit';
2323
import { VideoWeiXin } from './video/weixin';
24+
import { VideoKuaishou } from './video/kuaishou';
25+
import { DynamicKuaishou } from './dynamic/kuaishou';
2426

2527
export interface SyncData {
2628
platforms: string[];
@@ -258,6 +260,15 @@ export const infoMap: Record<string, PlatformInfo> = {
258260
injectUrl: 'https://www.reddit.com/submit?type=TEXT',
259261
injectFunction: DynamicReddit,
260262
},
263+
DYNAMIC_KUAISHOU: {
264+
type: 'DYNAMIC',
265+
name: 'DYNAMIC_KUAISHOU',
266+
homeUrl: 'https://cp.kuaishou.com/',
267+
faviconUrl: 'https://www.kuaishou.com/favicon.ico',
268+
platformName: chrome.i18n.getMessage('platformKuaishou'),
269+
injectUrl: 'https://cp.kuaishou.com/article/publish/video',
270+
injectFunction: DynamicKuaishou,
271+
},
261272
VIDEO_BILIBILI: {
262273
type: 'VIDEO',
263274
name: 'VIDEO_BILIBILI',
@@ -314,6 +325,15 @@ export const infoMap: Record<string, PlatformInfo> = {
314325
injectUrl: 'https://channels.weixin.qq.com/platform/post/create',
315326
injectFunction: VideoWeiXin,
316327
},
328+
VIDEO_KUAISHOU: {
329+
type: 'VIDEO',
330+
name: 'VIDEO_KUAISHOU',
331+
homeUrl: 'https://cp.kuaishou.com/',
332+
faviconUrl: 'https://www.kuaishou.com/favicon.ico',
333+
platformName: chrome.i18n.getMessage('platformKuaishou'),
334+
injectUrl: 'https://cp.kuaishou.com/article/publish/video',
335+
injectFunction: VideoKuaishou,
336+
},
317337
};
318338

319339
export function getDefaultPlatformInfo(platform: string): PlatformInfo | null {

src/sync/dynamic/kuaishou.ts

Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
1+
import type { DynamicData, SyncData } from '../common';
2+
3+
// 优先发布图文
4+
export async function DynamicKuaishou(data: SyncData) {
5+
const { title, content, images, videos } = data.data as DynamicData;
6+
// 辅助函数:等待元素出现
7+
function waitForElement(selector: string, timeout = 10000): Promise<Element> {
8+
return new Promise((resolve, reject) => {
9+
const element = document.querySelector(selector);
10+
if (element) {
11+
resolve(element);
12+
return;
13+
}
14+
15+
const observer = new MutationObserver(() => {
16+
const element = document.querySelector(selector);
17+
if (element) {
18+
resolve(element);
19+
observer.disconnect();
20+
}
21+
});
22+
23+
observer.observe(document.body, {
24+
childList: true,
25+
subtree: true,
26+
});
27+
28+
setTimeout(() => {
29+
observer.disconnect();
30+
reject(new Error(`Element with selector "${selector}" not found within ${timeout}ms`));
31+
}, timeout);
32+
});
33+
}
34+
35+
// 辅助函数:通过文本内容查找元素
36+
async function findElementByText(
37+
selector: string,
38+
text: string,
39+
maxRetries = 5,
40+
retryInterval = 1000,
41+
): Promise<Element | null> {
42+
for (let i = 0; i < maxRetries; i++) {
43+
const elements = document.querySelectorAll(selector);
44+
const element = Array.from(elements).find((element) => element.textContent?.includes(text));
45+
46+
if (element) {
47+
return element;
48+
}
49+
50+
console.log(`未找到包含文本 "${text}" 的元素,尝试次数:${i + 1}`);
51+
await new Promise((resolve) => setTimeout(resolve, retryInterval));
52+
}
53+
54+
console.error(`在 ${maxRetries} 次尝试后未找到包含文本 "${text}" 的元素`);
55+
return null;
56+
}
57+
58+
// 辅助函数:上传文件
59+
async function uploadImages() {
60+
const fileInput = (await waitForElement(
61+
'input[type="file"][accept="image/png, image/jpg, image/jpeg, image/webp"]',
62+
)) as HTMLInputElement;
63+
if (!fileInput) {
64+
console.error('未找到文件输入元素');
65+
return;
66+
}
67+
68+
const dataTransfer = new DataTransfer();
69+
70+
console.log('开始上传图片');
71+
for (const fileInfo of images) {
72+
console.log(`准备上传图片: ${fileInfo.url}`);
73+
try {
74+
const response = await fetch(fileInfo.url);
75+
if (!response.ok) {
76+
throw new Error(`HTTP 错误! 状态: ${response.status}`);
77+
}
78+
const blob = await response.blob();
79+
const file = new File([blob], fileInfo.name, { type: fileInfo.type });
80+
dataTransfer.items.add(file);
81+
} catch (error) {
82+
console.error(`上传图片 ${fileInfo.url} 失败:`, error);
83+
}
84+
}
85+
86+
if (dataTransfer.files.length > 0) {
87+
const uploadButton = (await findElementByText('button', '上传图片')) as HTMLElement;
88+
89+
// 使用 simulateDragAndDrop 函数模拟拖拽事件
90+
simulateDragAndDrop(uploadButton.parentElement.parentElement, dataTransfer);
91+
console.log('文件上传操作完成');
92+
} else {
93+
console.error('没有成功添加任何文件');
94+
}
95+
}
96+
97+
// 模拟拖拽事件的函数
98+
function simulateDragAndDrop(element: HTMLElement, dataTransfer: DataTransfer) {
99+
console.log('simulateDragAndDrop', dataTransfer);
100+
const events = [
101+
new DragEvent('dragenter', { bubbles: true }),
102+
new DragEvent('dragover', { bubbles: true }),
103+
new DragEvent('drop', { bubbles: true, dataTransfer: dataTransfer }),
104+
];
105+
events.forEach((event) => {
106+
Object.defineProperty(event, 'preventDefault', { value: () => {} });
107+
});
108+
events.forEach((event) => {
109+
console.log('event', event);
110+
element.dispatchEvent(event);
111+
});
112+
}
113+
114+
async function uploadVideo(file: File): Promise<void> {
115+
const fileInput = (await waitForElement(
116+
'input[type=file][accept="video/*,.mp4,.mov,.flv,.f4v,.webm,.mkv,.rm,.rmvb,.m4v,.3gp,.3g2,.wmv,.avi,.asf,.mpg,.mpeg,.ts"]',
117+
)) as HTMLInputElement;
118+
119+
// 创建一个新的 File 对象,因为某些浏览器可能不允许直接设置 fileInput.files
120+
const dataTransfer = new DataTransfer();
121+
dataTransfer.items.add(file);
122+
fileInput.files = dataTransfer.files;
123+
124+
// 触发 change 事件
125+
const changeEvent = new Event('change', { bubbles: true });
126+
fileInput.dispatchEvent(changeEvent);
127+
128+
console.log('视频上传事件已触发');
129+
}
130+
131+
if (images && images.length > 0) {
132+
console.log('检测到图片,开始上传');
133+
134+
const imageTab = (await waitForElement('div#rc-tabs-0-tab-2')) as HTMLElement;
135+
imageTab.click();
136+
await new Promise((resolve) => setTimeout(resolve, 2000));
137+
await uploadImages();
138+
139+
await new Promise((resolve) => setTimeout(resolve, 5000));
140+
141+
// 处理作品描述
142+
const contentEditor = (await waitForElement('div#work-description-edit[contenteditable="true"]')) as HTMLDivElement;
143+
if (contentEditor) {
144+
contentEditor.innerText = `${title || ''}\n\n${content}`;
145+
contentEditor.dispatchEvent(new Event('input', { bubbles: true }));
146+
}
147+
148+
// 等待内容更新
149+
await new Promise((resolve) => setTimeout(resolve, 3000));
150+
151+
// 自动选择前三个标签
152+
const recommendationTitle = await waitForElement('div._recommend-title_oei9t_269'); // 等待"话题推荐"标题出现
153+
const tagsContainer = recommendationTitle.nextElementSibling; // 获取下一个兄弟元素作为标签容器
154+
if (tagsContainer) {
155+
const tags = Array.from(tagsContainer.querySelectorAll('span._tag_oei9t_283')).filter((tag) => tag.textContent); // 获取所有标签
156+
if (tags.length > 0) {
157+
for (let i = 0; i < Math.min(3, tags.length); i++) {
158+
const tag = tags[i] as HTMLElement; // 类型断言为 HTMLElement
159+
tag.click(); // 点击选择标签
160+
}
161+
}
162+
}
163+
164+
if (data.auto_publish) {
165+
console.log('开始自动发布');
166+
const maxAttempts = 3;
167+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
168+
try {
169+
const publishButton = (await findElementByText('div', '发布')) as HTMLElement;
170+
publishButton.click();
171+
console.log('发布按钮已点击');
172+
await new Promise((resolve) => setTimeout(resolve, 3000));
173+
window.location.href = 'https://cp.kuaishou.com/article/manage/video';
174+
break; // 成功点击后退出循环
175+
} catch (error) {
176+
console.warn(`第 ${attempt + 1} 次尝试查找发布按钮失败:`, error);
177+
if (attempt === maxAttempts - 1) {
178+
console.error('达到最大尝试次数,无法找到发布按钮');
179+
}
180+
await new Promise((resolve) => setTimeout(resolve, 2000)); // 等待2秒后重试
181+
}
182+
}
183+
}
184+
} else if (videos && videos.length > 0) {
185+
console.log('检测到视频,开始上传');
186+
187+
// 点击切换到视频标签
188+
const videoTab = (await waitForElement('div[id="rc-tabs-0-tab-1"]')) as HTMLElement;
189+
videoTab.click();
190+
console.log('已点击视频标签');
191+
await new Promise((resolve) => setTimeout(resolve, 1000));
192+
193+
const video = videos[0];
194+
const response = await fetch(video.url);
195+
const blob = await response.blob();
196+
const videoFile = new File([blob], video.name, { type: video.type });
197+
console.log(`视频文件: ${videoFile.name} ${videoFile.type} ${videoFile.size}`);
198+
199+
await uploadVideo(videoFile);
200+
console.log('视频上传已初始化');
201+
202+
await new Promise((resolve) => setTimeout(resolve, 5000));
203+
204+
// 处理作品描述
205+
const contentEditor = (await waitForElement('div#work-description-edit[contenteditable="true"]')) as HTMLDivElement;
206+
if (contentEditor) {
207+
contentEditor.innerText = `${title || ''}\n\n${content}`;
208+
contentEditor.dispatchEvent(new Event('input', { bubbles: true }));
209+
}
210+
211+
// 等待内容更新
212+
await new Promise((resolve) => setTimeout(resolve, 3000));
213+
214+
// 自动选择前三个标签
215+
const recommendationTitle = await waitForElement('div._recommend-title_oei9t_269'); // 等待"话题推荐"标题出现
216+
const tagsContainer = recommendationTitle.nextElementSibling; // 获取下一个兄弟元素作为标签容器
217+
if (tagsContainer) {
218+
const tags = Array.from(tagsContainer.querySelectorAll('span._tag_oei9t_283')).filter((tag) => tag.textContent); // 获取所有标签
219+
if (tags.length > 0) {
220+
for (let i = 0; i < Math.min(3, tags.length); i++) {
221+
const tag = tags[i] as HTMLElement; // 类型断言为 HTMLElement
222+
tag.click(); // 点击选择标签
223+
}
224+
}
225+
}
226+
227+
// 等待内容更新
228+
await new Promise((resolve) => setTimeout(resolve, 3000));
229+
230+
if (data.auto_publish) {
231+
const maxAttempts = 3;
232+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
233+
try {
234+
const publishButton = (await findElementByText('div', '发布')) as HTMLElement;
235+
publishButton.click();
236+
console.log('发布按钮已点击');
237+
await new Promise((resolve) => setTimeout(resolve, 3000));
238+
window.location.href = 'https://cp.kuaishou.com/article/manage/video';
239+
break; // 成功点击后退出循环
240+
} catch (error) {
241+
console.warn(`第 ${attempt + 1} 次尝试查找发布按钮失败:`, error);
242+
if (attempt === maxAttempts - 1) {
243+
console.error('达到最大尝试次数,无法找到发布按钮');
244+
}
245+
await new Promise((resolve) => setTimeout(resolve, 2000)); // 等待2秒后重试
246+
}
247+
}
248+
}
249+
}
250+
}

0 commit comments

Comments
 (0)