Skip to content

Commit bfc88b3

Browse files
authored
70 feat support baijiahao (#71)
* feat: add Baijiahao platform support for dynamic content sync - Add localization for Baijiahao platform in English and Chinese - Implement dynamic content synchronization for Baijiahao platform - Register Baijiahao platform in sync configuration - Create dedicated sync function for Baijiahao dynamic content upload * feat: add Baijiahao video platform sync support - Implement video upload functionality for Baijiahao platform - Add platform configuration for Baijiahao video sync in common configuration - Create dedicated sync function for Baijiahao video content upload - Support file upload, description input, and optional auto-publishing * feat: add Baijiahao article platform sync support - Implement article upload functionality for Baijiahao platform - Add platform configuration for Baijiahao article sync in common configuration - Create dedicated sync function for Baijiahao article content upload - Support image processing, cover image handling, and content synchronization - Implement robust error handling and user feedback during article upload
1 parent 1c3162a commit bfc88b3

File tree

6 files changed

+641
-1
lines changed

6 files changed

+641
-1
lines changed

locales/en/messages.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -595,5 +595,9 @@
595595
"platformKuaishou": {
596596
"message": "Kuaishou",
597597
"description": "Platform name for Kuaishou"
598+
},
599+
"platformBaijiahao": {
600+
"message": "Baijiahao",
601+
"description": "Platform name for Baijiahao"
598602
}
599603
}

locales/zh_CN/messages.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -591,5 +591,9 @@
591591
"platformKuaishou": {
592592
"message": "快手",
593593
"description": "快手平台名称"
594+
},
595+
"platformBaijiahao": {
596+
"message": "百家号",
597+
"description": "百家号平台名称"
594598
}
595599
}

src/sync/article/baijiahao.ts

Lines changed: 352 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,352 @@
1+
import type { ArticleData, FileData, SyncData } from '~sync/common';
2+
3+
interface CoverResult {
4+
originSrc: string;
5+
src: string | null;
6+
cropParams: { x: number; y: number; w: number; h: number };
7+
ratio: number;
8+
}
9+
10+
export async function ArticleBaijiahao(data: SyncData) {
11+
console.log('ArticleBaijiahao', data);
12+
13+
const articleData = data.data as ArticleData;
14+
15+
// 上传单个图片
16+
async function uploadSingleImage(fileInfo: FileData): Promise<string | null> {
17+
try {
18+
const blob = await (await fetch(fileInfo.url)).blob();
19+
const file = new File([blob], fileInfo.name, { type: fileInfo.type });
20+
21+
const formData = new FormData();
22+
formData.append('org_file_name', fileInfo.name);
23+
formData.append('type', 'image');
24+
formData.append('app_id', '');
25+
formData.append('is_waterlog', '1');
26+
formData.append('save_material', '1');
27+
formData.append('no_compress', '0');
28+
formData.append('is_events', '');
29+
formData.append('article_type', 'news');
30+
formData.append('media', file);
31+
32+
const response = await fetch('https://baijiahao.baidu.com/materialui/picture/uploadProxy', {
33+
method: 'POST',
34+
body: formData,
35+
credentials: 'include',
36+
headers: {
37+
Token: getEditToken(),
38+
},
39+
});
40+
41+
if (!response.ok) {
42+
throw new Error(`上传失败: ${response.status}`);
43+
}
44+
45+
const result = await response.json();
46+
if (result?.ret?.https_url) {
47+
return result.ret.https_url;
48+
}
49+
return null;
50+
} catch (error) {
51+
console.error('上传图片失败:', error);
52+
return null;
53+
}
54+
}
55+
56+
// 获取编辑token
57+
function getEditToken(): string {
58+
const token = localStorage.getItem('edit-token')?.replace(/"/g, '');
59+
return token || '';
60+
}
61+
62+
// 裁剪图片
63+
async function cropImage(
64+
src: string,
65+
params: { x: number; y: number; w: number; h: number },
66+
): Promise<string | null> {
67+
try {
68+
const formData = new FormData();
69+
formData.append('auto', 'true');
70+
formData.append('x', params.x.toString());
71+
formData.append('y', params.y.toString());
72+
formData.append('w', params.w.toString());
73+
formData.append('h', params.h.toString());
74+
formData.append('src', src);
75+
formData.append('type', 'newsRow');
76+
formData.append('cutting_type', 'cover_image');
77+
78+
const response = await fetch('https://baijiahao.baidu.com/pcui/Picture/CuttingPicproxy', {
79+
method: 'POST',
80+
body: formData,
81+
});
82+
83+
if (!response.ok) {
84+
throw new Error(`裁剪失败: ${response.status}`);
85+
}
86+
87+
const result = await response.json();
88+
if (result.errno === 0 && result.data?.https_src) {
89+
return result.data.https_src;
90+
}
91+
throw new Error(result.errmsg || '裁剪图片失败');
92+
} catch (error) {
93+
console.error('裁剪图片失败:', error);
94+
return null;
95+
}
96+
}
97+
98+
// 处理文章内容中的图片
99+
async function processContent(content: string, fileDatas: FileData[]): Promise<string> {
100+
const parser = new DOMParser();
101+
const doc = parser.parseFromString(content, 'text/html');
102+
const images = Array.from(doc.getElementsByTagName('img'));
103+
104+
console.log(`处理文章图片,共 ${images.length} 张`);
105+
106+
const uploadPromises = images.map(async (img) => {
107+
const src = img.getAttribute('src');
108+
if (!src) return;
109+
110+
const fileInfo = fileDatas.find((f) => f.url === src);
111+
if (!fileInfo) return;
112+
113+
const newUrl = await uploadSingleImage(fileInfo);
114+
if (newUrl) {
115+
img.setAttribute('src', newUrl);
116+
} else {
117+
console.error(`图片处理失败: ${src}`);
118+
}
119+
});
120+
121+
await Promise.all(uploadPromises);
122+
return doc.body.innerHTML;
123+
}
124+
125+
// 处理封面图片上传和裁剪
126+
async function processCover(cover: FileData): Promise<CoverResult[] | null> {
127+
const coverUrl = await uploadSingleImage(cover);
128+
if (!coverUrl) return null;
129+
130+
// 获取图片实际尺寸
131+
const dimensions = await getImageDimensions(coverUrl);
132+
if (!dimensions) return null;
133+
134+
// 计算1.5和0.75比例的裁剪参数
135+
const horizontalCrop = calculateCropParams(dimensions.width, dimensions.height, 1.5);
136+
const verticalCrop = calculateCropParams(dimensions.width, dimensions.height, 0.75);
137+
138+
// 裁剪封面图
139+
const horizontalCoverUrl = await cropImage(coverUrl, horizontalCrop);
140+
const verticalCoverUrl = await cropImage(coverUrl, verticalCrop);
141+
142+
return [
143+
{ originSrc: coverUrl, src: horizontalCoverUrl, cropParams: horizontalCrop, ratio: 1.5 },
144+
{ originSrc: coverUrl, src: verticalCoverUrl, cropParams: verticalCrop, ratio: 0.75 },
145+
];
146+
}
147+
148+
// 获取图片实际尺寸
149+
async function getImageDimensions(url: string): Promise<{ width: number; height: number } | null> {
150+
return new Promise((resolve) => {
151+
const img = new Image();
152+
img.onload = () => {
153+
resolve({
154+
width: img.naturalWidth,
155+
height: img.naturalHeight,
156+
});
157+
};
158+
img.onerror = () => {
159+
console.error('获取图片尺寸失败:', url);
160+
resolve(null);
161+
};
162+
img.src = url;
163+
});
164+
}
165+
166+
// 发布文章
167+
async function publishArticle(articleData: ArticleData): Promise<string | null> {
168+
console.log('开始发布文章:', articleData.title);
169+
170+
if (articleData.fileDatas) {
171+
articleData.content = await processContent(articleData.content, articleData.fileDatas);
172+
}
173+
174+
let coverResults: CoverResult[] | null = null;
175+
if (articleData.cover) {
176+
coverResults = await processCover(articleData.cover);
177+
if (!coverResults) {
178+
console.error('封面处理失败');
179+
return null;
180+
}
181+
}
182+
183+
const formData = new FormData();
184+
formData.append('type', 'news');
185+
formData.append('title', articleData.title?.slice(0, 30) || '');
186+
formData.append('content', articleData.content || '');
187+
formData.append('vertical_cover', coverResults?.[1].src || '');
188+
formData.append('abstract', articleData.digest || '');
189+
190+
const contentLength =
191+
new DOMParser().parseFromString(articleData.content || '', 'text/html').documentElement.textContent?.length || 0;
192+
193+
formData.append('len', contentLength.toString());
194+
formData.append('activity_list[0][id]', 'ttv');
195+
formData.append('activity_list[0][is_checked]', '1');
196+
formData.append('activity_list[1][id]', 'reward');
197+
formData.append('activity_list[1][is_checked]', '1');
198+
formData.append('activity_list[2][id]', 'aigc_bjh_status');
199+
formData.append('activity_list[2][is_checked]', '0');
200+
formData.append('source_reprinted_allow', '0');
201+
formData.append('is_auto_optimize_cover', '1');
202+
formData.append('abstract_from', '1');
203+
formData.append('cover_layout', 'one');
204+
205+
if (coverResults) {
206+
formData.append(
207+
'cover_images',
208+
JSON.stringify([
209+
{
210+
src: coverResults[0].src,
211+
cropData: {
212+
x: coverResults[0].cropParams.x,
213+
y: coverResults[0].cropParams.y,
214+
width: coverResults[0].cropParams.w,
215+
height: coverResults[0].cropParams.h,
216+
},
217+
machine_chooseimg: 0,
218+
isLegal: 0,
219+
cover_source_tag: 'local',
220+
},
221+
]),
222+
);
223+
224+
formData.append(
225+
'_cover_images_map',
226+
JSON.stringify([
227+
{
228+
src: coverResults[0].src,
229+
origin_src: coverResults[0].originSrc,
230+
},
231+
]),
232+
);
233+
}
234+
235+
formData.append('source', 'upload');
236+
formData.append('cover_source', 'upload');
237+
238+
try {
239+
const response = await fetch('https://baijiahao.baidu.com/pcui/article/save?callback=bjhdraft', {
240+
method: 'POST',
241+
body: formData,
242+
credentials: 'include',
243+
headers: {
244+
Token: getEditToken(),
245+
},
246+
});
247+
248+
if (!response.ok) {
249+
throw new Error(`发布失败: ${response.status}`);
250+
}
251+
252+
const result = await response.json();
253+
if (result.errno === 0) {
254+
console.log('文章发布成功,ID:', result.ret?.id);
255+
return result.ret?.id;
256+
} else {
257+
console.error('发布失败:', result.message);
258+
return null;
259+
}
260+
} catch (error) {
261+
console.error('发布过程出错:', error);
262+
return null;
263+
}
264+
}
265+
266+
// 计算裁剪参数
267+
function calculateCropParams(width: number, height: number, ratio: number) {
268+
let w, h, x, y;
269+
if (width / height > ratio) {
270+
h = height;
271+
w = Math.floor(height * ratio);
272+
y = 0;
273+
x = Math.floor((width - w) / 2);
274+
} else {
275+
w = width;
276+
h = Math.floor(width / ratio);
277+
x = 0;
278+
y = Math.floor((height - h) / 2);
279+
}
280+
return { x, y, w, h };
281+
}
282+
283+
// 主流程
284+
const host = document.createElement('div') as HTMLDivElement;
285+
const tip = document.createElement('div') as HTMLDivElement;
286+
287+
try {
288+
// 添加漂浮提示
289+
host.style.position = 'fixed';
290+
host.style.bottom = '20px';
291+
host.style.right = '20px';
292+
host.style.zIndex = '9999';
293+
document.body.appendChild(host);
294+
295+
const shadow = host.attachShadow({ mode: 'open' });
296+
297+
tip.innerHTML = `
298+
<style>
299+
.float-tip {
300+
background: #1e293b;
301+
color: white;
302+
padding: 12px 16px;
303+
border-radius: 8px;
304+
font-size: 14px;
305+
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1);
306+
animation: slideIn 0.3s ease-out;
307+
}
308+
@keyframes slideIn {
309+
from {
310+
transform: translateY(100%);
311+
opacity: 0;
312+
}
313+
to {
314+
transform: translateY(0);
315+
opacity: 1;
316+
}
317+
}
318+
</style>
319+
<div class="float-tip">
320+
正在同步文章到百度百家号...
321+
</div>
322+
`;
323+
shadow.appendChild(tip);
324+
325+
const articleId = await publishArticle(articleData);
326+
327+
if (articleId) {
328+
(tip.querySelector('.float-tip') as HTMLDivElement).textContent = '文章同步成功!';
329+
330+
setTimeout(() => {
331+
document.body.removeChild(host);
332+
}, 3000);
333+
334+
if (!data.auto_publish) {
335+
window.location.href = `https://baijiahao.baidu.com/builder/rc/edit?type=news&article_id=${articleId}`;
336+
}
337+
}
338+
} catch (error) {
339+
if (document.body.contains(host)) {
340+
const floatTip = tip.querySelector('.float-tip') as HTMLDivElement;
341+
floatTip.textContent = '同步失败,请重试';
342+
floatTip.style.backgroundColor = '#dc2626';
343+
344+
setTimeout(() => {
345+
document.body.removeChild(host);
346+
}, 3000);
347+
}
348+
349+
console.error('发布文章失败:', error);
350+
throw error;
351+
}
352+
}

0 commit comments

Comments
 (0)