publish_long_article.py
8.17 KB
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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
"""长文发布模式,参考 cdp_publish.py 的长文工作流。"""
from __future__ import annotations
import json
import logging
import time
from .cdp import Page
from .errors import PublishError
from .publish import _click_publish_tab, _find_content_element, _navigate_to_publish_page
from .selectors import (
AUTO_FORMAT_BUTTON_TEXT,
CONTENT_EDITOR,
LONG_ARTICLE_TITLE,
NEW_CREATION_BUTTON_TEXT,
NEXT_STEP_BUTTON_TEXT,
TEMPLATE_CARD,
TEMPLATE_TITLE,
)
logger = logging.getLogger(__name__)
# 等待常量
_AUTO_FORMAT_WAIT = 3.0
_TEMPLATE_WAIT_ROUNDS = 15
_PAGE_LOAD_WAIT = 3.0
def publish_long_article(
page: Page,
title: str,
content: str,
image_paths: list[str] | None = None,
) -> list[str]:
"""长文发布:导航 → 点击写长文 → 新的创作 → 填写标题正文 → 一键排版。
返回可用模板名称列表。
Args:
page: CDP 页面对象。
title: 长文标题。
content: 长文正文(段落用换行分隔)。
image_paths: 可选的图片路径列表(插入编辑器)。
Returns:
可用模板名称列表。
Raises:
PublishError: 操作失败。
"""
# 1. 导航到发布页
_navigate_to_publish_page(page)
# 2. 点击"写长文"TAB
_click_publish_tab(page, "写长文")
time.sleep(1)
# 3. 点击"新的创作"
_click_new_creation(page)
# 4. 填写标题(textarea)
_fill_long_title(page, title)
# 5. 填写正文(TipTap 编辑器)
_fill_long_content(page, content)
# 6. 可选:插入图片到编辑器
if image_paths:
_insert_images_to_editor(page, image_paths)
# 7. 点击"一键排版"
_click_auto_format(page)
# 8. 等待模板加载并返回名称列表
_wait_for_templates(page)
template_names = get_template_names(page)
logger.info("模板加载完成: %s", template_names)
return template_names
def get_template_names(page: Page) -> list[str]:
"""获取当前可用的排版模板名称列表。
Args:
page: CDP 页面对象。
Returns:
模板名称列表。
"""
names = page.evaluate(
f"""
(() => {{
const cards = document.querySelectorAll({json.dumps(TEMPLATE_CARD)});
const names = [];
for (const card of cards) {{
const title = card.querySelector({json.dumps(TEMPLATE_TITLE)});
names.push(title ? title.textContent.trim() : 'Template ' + names.length);
}}
return names;
}})()
"""
)
return names or []
def select_template(page: Page, template_name: str) -> bool:
"""选择指定名称的排版模板。
Args:
page: CDP 页面对象。
template_name: 模板名称。
Returns:
是否成功选择。
"""
clicked = page.evaluate(
f"""
(() => {{
const cards = document.querySelectorAll({json.dumps(TEMPLATE_CARD)});
for (const card of cards) {{
const title = card.querySelector({json.dumps(TEMPLATE_TITLE)});
if (title && title.textContent.trim() === {json.dumps(template_name)}) {{
card.click();
return true;
}}
}}
return false;
}})()
"""
)
if clicked:
logger.info("已选择模板: %s", template_name)
time.sleep(1)
else:
logger.warning("未找到模板: %s", template_name)
return bool(clicked)
def click_next_and_fill_description(page: Page, description: str) -> None:
"""点击下一步,进入发布页并填写正文描述。
注意:发布页有独立的正文编辑器,需单独填入。
如果 description 超过 1000 字,应压缩到 800 字左右。
Args:
page: CDP 页面对象。
description: 发布页正文描述。
Raises:
PublishError: 操作失败。
"""
# 点击"下一步"
_click_button_by_text(page, NEXT_STEP_BUTTON_TEXT)
time.sleep(_PAGE_LOAD_WAIT)
# 填写发布页描述
if description:
# 截断描述到 1000 字以内
if len(description) > 1000:
description = description[:800]
logger.warning("描述超过1000字,已截断到800字")
content_selector = _find_content_element(page)
page.input_content_editable(content_selector, description)
logger.info("已填写发布页描述")
# ========== 内部辅助函数 ==========
def _click_new_creation(page: Page) -> None:
"""点击"新的创作"按钮。"""
_click_button_by_text(page, NEW_CREATION_BUTTON_TEXT)
time.sleep(2)
page.wait_dom_stable()
logger.info("已点击'新的创作'")
def _fill_long_title(page: Page, title: str) -> None:
"""填写长文标题(textarea,需使用 native setter)。"""
page.wait_for_element(LONG_ARTICLE_TITLE, timeout=10)
page.evaluate(
f"""
(() => {{
const el = document.querySelector({json.dumps(LONG_ARTICLE_TITLE)});
if (!el) return false;
const nativeSetter = Object.getOwnPropertyDescriptor(
window.HTMLTextAreaElement.prototype, 'value'
).set;
el.focus();
nativeSetter.call(el, {json.dumps(title)});
el.dispatchEvent(new Event('input', {{ bubbles: true }}));
el.dispatchEvent(new Event('change', {{ bubbles: true }}));
return true;
}})()
"""
)
logger.info("已填写长文标题: %s", title[:20])
time.sleep(0.5)
def _fill_long_content(page: Page, content: str) -> None:
"""填写长文正文(TipTap/ProseMirror 编辑器)。"""
content_selector = CONTENT_EDITOR
if not page.has_element(CONTENT_EDITOR):
content_selector = _find_content_element(page)
page.input_content_editable(content_selector, content)
logger.info("已填写长文正文 (%d 字)", len(content))
time.sleep(1)
def _insert_images_to_editor(page: Page, image_paths: list[str]) -> None:
"""将图片插入到编辑器中。"""
for img_path in image_paths:
normalized = img_path.replace("\\", "/")
page.evaluate(
f"""
(() => {{
const editor = document.querySelector({json.dumps(CONTENT_EDITOR)});
if (!editor) return false;
const img = document.createElement('img');
img.src = 'file:///' + {json.dumps(normalized)};
editor.appendChild(img);
editor.dispatchEvent(new Event('input', {{ bubbles: true }}));
return true;
}})()
"""
)
logger.info("已插入 %d 张图片到编辑器", len(image_paths))
time.sleep(1)
def _click_auto_format(page: Page) -> None:
"""点击"一键排版"按钮。"""
_click_button_by_text(page, AUTO_FORMAT_BUTTON_TEXT)
logger.info("已点击'一键排版',等待模板加载...")
time.sleep(_AUTO_FORMAT_WAIT)
def _wait_for_templates(page: Page) -> bool:
"""等待模板卡片出现。"""
for _ in range(_TEMPLATE_WAIT_ROUNDS):
count = page.get_elements_count(TEMPLATE_CARD)
if count and count > 0:
logger.info("发现 %d 个模板卡片", count)
return True
time.sleep(1)
logger.warning("等待模板卡片超时")
return False
def _click_button_by_text(page: Page, text: str) -> None:
"""通过文本内容查找并点击按钮(通用方法)。"""
clicked = page.evaluate(
f"""
(() => {{
const elems = document.querySelectorAll(
'button, [role="button"], span, div, a, [class*="btn"]'
);
for (const el of elems) {{
if (el.textContent.trim() === {json.dumps(text)}) {{
const rect = el.getBoundingClientRect();
if (rect.width === 0 || rect.height === 0) continue;
el.click();
return true;
}}
}}
return false;
}})()
"""
)
if not clicked:
raise PublishError(f"未找到'{text}'按钮,页面结构可能已变化")