@qcplay/cli 1.0.15 → 1.0.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/content-rules.js +118 -15
- package/lib/platform-publish.js +26 -6
- package/lib/wechat-article.js +8 -0
- package/package.json +1 -1
package/lib/content-rules.js
CHANGED
|
@@ -14,6 +14,10 @@ const RULE_ACTIONS = new Set([
|
|
|
14
14
|
"remove_text_block",
|
|
15
15
|
"replace_text",
|
|
16
16
|
"remove_image",
|
|
17
|
+
"remove_first_image",
|
|
18
|
+
"remove_last_image",
|
|
19
|
+
"remove_images_between_text",
|
|
20
|
+
"remove_first_video",
|
|
17
21
|
"replace_image",
|
|
18
22
|
"remove_image_caption"
|
|
19
23
|
]);
|
|
@@ -168,7 +172,15 @@ function validateAction(action, location) {
|
|
|
168
172
|
const imageFields = new Set(["src", "alt", "title", "caption", "nearby_text"]);
|
|
169
173
|
const removableImageFields = new Set([...imageFields, "sha256"]);
|
|
170
174
|
const textFields = new Set(["text"]);
|
|
171
|
-
if (
|
|
175
|
+
if (["remove_first_image", "remove_last_image", "remove_first_video"].includes(action.type)) {
|
|
176
|
+
if (action.match !== undefined) ruleError(location, `${action.type} 不支持 match`);
|
|
177
|
+
} else if (action.type === "remove_images_between_text") {
|
|
178
|
+
validateMatch(action.match, `${location}.match`, textFields);
|
|
179
|
+
validateMatch(action.end_match, `${location}.end_match`, textFields);
|
|
180
|
+
if (action.match.text === undefined || action.end_match.text === undefined) {
|
|
181
|
+
ruleError(location, "remove_images_between_text 必须提供起止 text");
|
|
182
|
+
}
|
|
183
|
+
} else if (action.type === "remove_image_caption") {
|
|
172
184
|
validateMatch(action.match || {}, `${location}.match`, imageFields);
|
|
173
185
|
} else if (action.type === "remove_image" || action.type === "replace_image") {
|
|
174
186
|
validateMatch(action.match, `${location}.match`, action.type === "remove_image" ? removableImageFields : imageFields);
|
|
@@ -343,7 +355,7 @@ function docxImageHashes(buffer) {
|
|
|
343
355
|
const commentLength = buffer.readUInt16LE(offset + 32);
|
|
344
356
|
const localHeaderOffset = buffer.readUInt32LE(offset + 42);
|
|
345
357
|
const fileName = buffer.subarray(offset + 46, offset + 46 + fileNameLength).toString("utf8");
|
|
346
|
-
if (/^word\/media
|
|
358
|
+
if (/^word\/media\/[^/]+$/i.test(fileName)) {
|
|
347
359
|
if (buffer.readUInt32LE(localHeaderOffset) !== 0x04034b50) throw new Error("DOCX 条目无效");
|
|
348
360
|
const localNameLength = buffer.readUInt16LE(localHeaderOffset + 26);
|
|
349
361
|
const localExtraLength = buffer.readUInt16LE(localHeaderOffset + 28);
|
|
@@ -368,10 +380,9 @@ function parseRuleText(text, file, imageHashes = []) {
|
|
|
368
380
|
const fields = { "规则名称": "name", "项目": "project", "地区": "region", "平台": "platform", "来源": "source", "阶段": "phase" };
|
|
369
381
|
const ignored = /^(?:#|\/\/|说明[::]?|其他要求[::]?)\s*/;
|
|
370
382
|
const unsupported = [];
|
|
371
|
-
let hasImageHashRule = false;
|
|
372
383
|
for (const rawLine of trimmed.split(/\r?\n/)) {
|
|
373
384
|
const line = rawLine.trim().replace(/^(?:[-*•]\s*|\d+[.、]\s*)/, "");
|
|
374
|
-
if (!line || ignored.test(line) || /(?:样式全部保留|样式.*保留不变|区分.*b站.*非b
|
|
385
|
+
if (!line || ignored.test(line) || /(?:样式全部保留|样式.*保留不变|区分.*b站.*非b站|可以有两篇文章|^文章.*规(?:则)?(?:修改)?$)/i.test(line)) continue;
|
|
375
386
|
if (line === "最强蜗牛") {
|
|
376
387
|
rule.project = line;
|
|
377
388
|
continue;
|
|
@@ -412,21 +423,51 @@ function parseRuleText(text, file, imageHashes = []) {
|
|
|
412
423
|
}
|
|
413
424
|
const imageSection = /^(?:b站的内容要删除如下图片|删除图片)\s*[::]?\s*$/i.test(line);
|
|
414
425
|
if (imageSection) {
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
426
|
+
rule.actions.push(...imageHashes.map(sha256 => ({ type: "remove_image", match: { sha256 } })));
|
|
427
|
+
continue;
|
|
428
|
+
}
|
|
429
|
+
if (/^删除第一张图片\s*[::]?\s*$/i.test(line)) {
|
|
430
|
+
rule.actions.push({ type: "remove_first_image" });
|
|
431
|
+
continue;
|
|
432
|
+
}
|
|
433
|
+
if (/^删除最后一张图片\s*[::]?\s*$/i.test(line)) {
|
|
434
|
+
rule.actions.push({ type: "remove_last_image" });
|
|
435
|
+
continue;
|
|
436
|
+
}
|
|
437
|
+
const imagesBetween = line.match(/^删除(?:从)?\s*(?!图片[::])(.+?)\s*(?:到|至)\s*(.+?)\s*中间的图片$/);
|
|
438
|
+
if (imagesBetween) {
|
|
439
|
+
rule.actions.push({
|
|
440
|
+
type: "remove_images_between_text",
|
|
441
|
+
match: { text: imagesBetween[1].trim(), operator: "contains" },
|
|
442
|
+
end_match: { text: imagesBetween[2].trim(), operator: "contains" }
|
|
443
|
+
});
|
|
425
444
|
continue;
|
|
426
445
|
}
|
|
427
446
|
const image = line.match(/^删除图片[::]\s*(.+)$/);
|
|
428
447
|
if (image) {
|
|
429
|
-
|
|
448
|
+
const instruction = image[1].trim();
|
|
449
|
+
if (/^最后一张图片$/i.test(instruction)) {
|
|
450
|
+
rule.actions.push({ type: "remove_last_image" });
|
|
451
|
+
continue;
|
|
452
|
+
}
|
|
453
|
+
if (/^第一张图片$/i.test(instruction)) {
|
|
454
|
+
rule.actions.push({ type: "remove_first_image" });
|
|
455
|
+
continue;
|
|
456
|
+
}
|
|
457
|
+
const nestedImagesBetween = instruction.match(/^删除(?:从)?\s*(.+?)\s*(?:到|至)\s*(.+?)\s*中间的图片$/);
|
|
458
|
+
if (nestedImagesBetween) {
|
|
459
|
+
rule.actions.push({
|
|
460
|
+
type: "remove_images_between_text",
|
|
461
|
+
match: { text: nestedImagesBetween[1].trim(), operator: "contains" },
|
|
462
|
+
end_match: { text: nestedImagesBetween[2].trim(), operator: "contains" }
|
|
463
|
+
});
|
|
464
|
+
continue;
|
|
465
|
+
}
|
|
466
|
+
rule.actions.push({ type: "remove_image", match: { nearby_text: instruction, operator: "contains" } });
|
|
467
|
+
continue;
|
|
468
|
+
}
|
|
469
|
+
if (/^删除视频[::]\s*第一个视频$/i.test(line)) {
|
|
470
|
+
rule.actions.push({ type: "remove_first_video" });
|
|
430
471
|
continue;
|
|
431
472
|
}
|
|
432
473
|
unsupported.push(rawLine.trim());
|
|
@@ -693,6 +734,22 @@ function replaceTextNodes($, match, replacement) {
|
|
|
693
734
|
return changed;
|
|
694
735
|
}
|
|
695
736
|
|
|
737
|
+
function documentOrder($) {
|
|
738
|
+
const nodes = [];
|
|
739
|
+
const visit = node => {
|
|
740
|
+
nodes.push(node);
|
|
741
|
+
for (const child of node.children || []) visit(child);
|
|
742
|
+
};
|
|
743
|
+
for (const node of $.root().contents().toArray()) visit(node);
|
|
744
|
+
return nodes;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
function firstMatchingContentBlock($, match) {
|
|
748
|
+
return $(BLOCK_SELECTOR)
|
|
749
|
+
.toArray()
|
|
750
|
+
.find(node => matchesValue($(node).text().trim(), match.text, match));
|
|
751
|
+
}
|
|
752
|
+
|
|
696
753
|
function applyAction($, action) {
|
|
697
754
|
if (action.type === "remove_section_until_next_heading") {
|
|
698
755
|
let changed = false;
|
|
@@ -713,6 +770,44 @@ function applyAction($, action) {
|
|
|
713
770
|
if (action.type === "replace_text") {
|
|
714
771
|
return replaceTextNodes($, action.match, action.replacement);
|
|
715
772
|
}
|
|
773
|
+
if (action.type === "remove_first_image") {
|
|
774
|
+
const firstImage = $("img").first();
|
|
775
|
+
if (!firstImage.length) return false;
|
|
776
|
+
removeImage($, firstImage);
|
|
777
|
+
return true;
|
|
778
|
+
}
|
|
779
|
+
if (action.type === "remove_last_image") {
|
|
780
|
+
const lastImage = $("img").last();
|
|
781
|
+
if (!lastImage.length) return false;
|
|
782
|
+
removeImage($, lastImage);
|
|
783
|
+
return true;
|
|
784
|
+
}
|
|
785
|
+
if (action.type === "remove_first_video") {
|
|
786
|
+
const firstVideo = $("video,mp-common-videosnap").first();
|
|
787
|
+
if (!firstVideo.length) return false;
|
|
788
|
+
const container = firstVideo.closest("p,figure,div,section").first();
|
|
789
|
+
if (container.length && !container.text().trim() && container.find("video,mp-common-videosnap").length === 1) {
|
|
790
|
+
container.remove();
|
|
791
|
+
} else {
|
|
792
|
+
firstVideo.remove();
|
|
793
|
+
}
|
|
794
|
+
return true;
|
|
795
|
+
}
|
|
796
|
+
if (action.type === "remove_images_between_text") {
|
|
797
|
+
const startNode = firstMatchingContentBlock($, action.match);
|
|
798
|
+
const endNode = firstMatchingContentBlock($, action.end_match);
|
|
799
|
+
if (!startNode || !endNode) return false;
|
|
800
|
+
const nodes = documentOrder($);
|
|
801
|
+
const start = nodes.indexOf(startNode);
|
|
802
|
+
const end = nodes.indexOf(endNode);
|
|
803
|
+
if (start < 0 || end < 0 || start >= end) return false;
|
|
804
|
+
const images = $("img").toArray().filter(node => {
|
|
805
|
+
const position = nodes.indexOf(node);
|
|
806
|
+
return position > start && position < end;
|
|
807
|
+
});
|
|
808
|
+
images.forEach(node => removeImage($, $(node)));
|
|
809
|
+
return images.length > 0;
|
|
810
|
+
}
|
|
716
811
|
const images = $("img").toArray().filter(node => imageMatches($, $(node), action.match || {}));
|
|
717
812
|
if (action.type === "remove_image") {
|
|
718
813
|
images.forEach(node => removeImage($, $(node)));
|
|
@@ -756,6 +851,10 @@ export function applyContentRules(article, entry = {}, config = entry.contentRul
|
|
|
756
851
|
"remove_text_block",
|
|
757
852
|
"replace_text",
|
|
758
853
|
"remove_image",
|
|
854
|
+
"remove_first_image",
|
|
855
|
+
"remove_last_image",
|
|
856
|
+
"remove_images_between_text",
|
|
857
|
+
"remove_first_video",
|
|
759
858
|
"replace_image",
|
|
760
859
|
"remove_image_caption"
|
|
761
860
|
]);
|
|
@@ -767,6 +866,10 @@ export function applyContentRules(article, entry = {}, config = entry.contentRul
|
|
|
767
866
|
if (options.phase === "import") {
|
|
768
867
|
const priority = {
|
|
769
868
|
remove_image: 0,
|
|
869
|
+
remove_first_image: 0,
|
|
870
|
+
remove_last_image: 0,
|
|
871
|
+
remove_images_between_text: 0,
|
|
872
|
+
remove_first_video: 0,
|
|
770
873
|
replace_image: 0,
|
|
771
874
|
remove_image_caption: 0,
|
|
772
875
|
remove_section_until_next_heading: 1,
|
package/lib/platform-publish.js
CHANGED
|
@@ -225,6 +225,9 @@ const BROWSER_PLATFORM_SPECS = {
|
|
|
225
225
|
collection: [
|
|
226
226
|
'input[placeholder*="搜索合集"]',
|
|
227
227
|
'input[placeholder*="合集"]',
|
|
228
|
+
'[role="dialog"] input:not([type="file"]):not([type="hidden"])',
|
|
229
|
+
'[class*="modal"] input:not([type="file"]):not([type="hidden"])',
|
|
230
|
+
'[class*="dialog"] input:not([type="file"]):not([type="hidden"])',
|
|
228
231
|
'[class*="collection"] input',
|
|
229
232
|
'[class*="album"] input'
|
|
230
233
|
],
|
|
@@ -242,6 +245,10 @@ function normalizeText(value) {
|
|
|
242
245
|
return String(value ?? "").trim();
|
|
243
246
|
}
|
|
244
247
|
|
|
248
|
+
function escapeRegExp(value) {
|
|
249
|
+
return String(value ?? "").replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
250
|
+
}
|
|
251
|
+
|
|
245
252
|
function normalizeKey(value) {
|
|
246
253
|
return normalizeText(value).toLowerCase().replace(/\s+/g, "");
|
|
247
254
|
}
|
|
@@ -6150,7 +6157,7 @@ async function publishXiaohongshu(page, entry, article, spec, options, promptUse
|
|
|
6150
6157
|
}
|
|
6151
6158
|
let imageInput = await firstExisting(page, spec.imageInputs);
|
|
6152
6159
|
if (!imageInput) {
|
|
6153
|
-
if (
|
|
6160
|
+
if (shouldWaitForXiaohongshuLogin(options)) {
|
|
6154
6161
|
await openPlatformLogin(page, entry, spec);
|
|
6155
6162
|
options.onProgress?.("小红书尚未登录,请在已打开的浏览器中扫码;登录完成后将自动继续上传图片");
|
|
6156
6163
|
imageInput = await waitForXiaohongshuUploadInput(page, spec, 600000);
|
|
@@ -6187,13 +6194,22 @@ async function publishXiaohongshu(page, entry, article, spec, options, promptUse
|
|
|
6187
6194
|
await fillLocator(titleInput, article.title);
|
|
6188
6195
|
await fillLocator(bodyInput, body);
|
|
6189
6196
|
if (publishing.collection) {
|
|
6190
|
-
const collectionTrigger =
|
|
6197
|
+
const collectionTrigger =
|
|
6198
|
+
(await waitForVisibleButton(page, [/添加(?:到)?合集/, /选择合集/, /加入合集/, /^合集$/], 3000)) ||
|
|
6199
|
+
(await waitForVisibleTextAcrossFrames(page, /添加(?:到)?合集|选择合集|加入合集|^合集$/, 3000, false));
|
|
6191
6200
|
if (!collectionTrigger) throw new Error("小红书页面未找到“添加合集”控件");
|
|
6192
6201
|
await collectionTrigger.click();
|
|
6193
6202
|
const collectionInput = await waitForVisible(page, spec.collection, 5000);
|
|
6194
|
-
if (
|
|
6195
|
-
|
|
6196
|
-
|
|
6203
|
+
if (collectionInput) {
|
|
6204
|
+
await fillLocator(collectionInput, publishing.collection);
|
|
6205
|
+
if (typeof collectionInput.press === "function") {
|
|
6206
|
+
await collectionInput.press("Enter").catch(() => {});
|
|
6207
|
+
}
|
|
6208
|
+
}
|
|
6209
|
+
await page.waitForTimeout(500);
|
|
6210
|
+
const collectionOption =
|
|
6211
|
+
(await waitForVisibleButton(page, [new RegExp(`^${escapeRegExp(publishing.collection)}$`)], 8000)) ||
|
|
6212
|
+
(await waitForVisibleTextAcrossFrames(page, publishing.collection, 8000));
|
|
6197
6213
|
if (!collectionOption) throw new Error(`小红书未找到精确合集: ${publishing.collection}`);
|
|
6198
6214
|
await collectionOption.click();
|
|
6199
6215
|
}
|
|
@@ -6206,7 +6222,7 @@ async function publishXiaohongshu(page, entry, article, spec, options, promptUse
|
|
|
6206
6222
|
options.onProgress?.("小红书待发布内容已准备完成,请在浏览器中核对后手动点击“发布”;程序将等待并验证发布结果");
|
|
6207
6223
|
return waitForManualXiaohongshuSubmission(page, 600000);
|
|
6208
6224
|
}
|
|
6209
|
-
await reviewPreparedDraft(page, entry,
|
|
6225
|
+
await reviewPreparedDraft(page, entry, options);
|
|
6210
6226
|
const initialUrl = page.url();
|
|
6211
6227
|
const mutationPromise =
|
|
6212
6228
|
typeof page.waitForResponse === "function"
|
|
@@ -6223,6 +6239,10 @@ function isInteractiveTerminal(streams = {}) {
|
|
|
6223
6239
|
return Boolean(input.isTTY && output.isTTY);
|
|
6224
6240
|
}
|
|
6225
6241
|
|
|
6242
|
+
export function shouldWaitForXiaohongshuLogin(options = {}) {
|
|
6243
|
+
return !isInteractiveTerminal(options.streams);
|
|
6244
|
+
}
|
|
6245
|
+
|
|
6226
6246
|
async function waitForXiaohongshuUploadInput(page, spec, timeoutMs) {
|
|
6227
6247
|
const deadline = Date.now() + timeoutMs;
|
|
6228
6248
|
do {
|
package/lib/wechat-article.js
CHANGED
|
@@ -1386,6 +1386,14 @@ export async function convertWechatHtml(html, pageUrl, options = {}) {
|
|
|
1386
1386
|
const key = assertWechatImageUrl(rawSource, sourceUrl).toString();
|
|
1387
1387
|
return uploaded.get(key);
|
|
1388
1388
|
});
|
|
1389
|
+
const retainedSources = entry.sources.filter(rawSource => {
|
|
1390
|
+
const key = assertWechatImageUrl(rawSource, sourceUrl).toString();
|
|
1391
|
+
return !ignored.has(key);
|
|
1392
|
+
});
|
|
1393
|
+
if (retainedSources.length === 0) {
|
|
1394
|
+
$(entry.element).css("background-image", "none");
|
|
1395
|
+
return;
|
|
1396
|
+
}
|
|
1389
1397
|
$(entry.element).attr("data-qcplay-background-processed", "1");
|
|
1390
1398
|
$(entry.element).attr("data-qcplay-background-srcs", urls.filter(Boolean).join("|"));
|
|
1391
1399
|
});
|