@gobing-ai/knowledge-kit 0.1.12 → 0.1.14
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/.claude-plugin/marketplace.json +1 -1
- package/dist/index.js +16 -4
- package/package.json +1 -1
- package/plugins/kk/plugin.json +1 -1
- package/plugins/kk/publish-presets/daily-ai-voice-migration.json +3 -3
- package/plugins/kk/publish-presets/daily-ai-voice.json +4 -4
- package/plugins/kk/schedules/kk-daily-publish3-cn-peak.cron.tmpl +2 -2
- package/plugins/kk/scripts/daily-scheduled-run.ts +138 -44
- package/plugins/kk/scripts/daily-stages.ts +52 -9
- package/plugins/kk/skills/content-safety/references/platforms/podcast-salty.md +41 -0
- package/plugins/kk/skills/content-safety/references/platforms/surfdash.md +36 -0
- package/plugins/kk/skills/content-safety/references/platforms/xhs.md +39 -0
- package/plugins/kk/workflows/kk-collect.yaml +3 -3
- package/plugins/kk/workflows/kk-daily-ai-voice2.yaml +12 -12
- package/plugins/kk/workflows/kk-daily-ai-voice3.yaml +2 -2
- package/plugins/publishings/surfdash-pub/dist/index.js +52 -23
- package/plugins/publishings/surfdash-pub/src/index.ts +69 -24
- package/plugins/publishings/wechatmp-pub/dist/index.js +171 -30
- package/plugins/publishings/wechatmp-pub/src/flow.ts +287 -49
- package/plugins/publishings/weibo-pub/dist/index.js +114 -8
- package/plugins/publishings/weibo-pub/src/index.ts +140 -16
- package/plugins/publishings/xhs-pub/dist/index.js +164 -12
- package/plugins/publishings/xhs-pub/dist/profile-check.js +11 -1
- package/plugins/publishings/xhs-pub/src/flow.ts +194 -23
- package/plugins/publishings/xhs-pub/src/page.ts +26 -0
|
@@ -20991,7 +20991,7 @@ function normalizeText(value) {
|
|
|
20991
20991
|
return value.replace(/\s+/g, "");
|
|
20992
20992
|
}
|
|
20993
20993
|
function excerpt(text) {
|
|
20994
|
-
return normalizeText(text).slice(0, EXCERPT_CHARS);
|
|
20994
|
+
return normalizeText(text.replace(/^[#>*`\-\s]+/gm, "")).slice(0, EXCERPT_CHARS);
|
|
20995
20995
|
}
|
|
20996
20996
|
function resolveOperation(options, content) {
|
|
20997
20997
|
const raw = options?.operation ?? content?.metadata?.operation ?? "post";
|
|
@@ -21118,6 +21118,9 @@ class WeiboBrowserTransport {
|
|
|
21118
21118
|
async runArticleFlow(page, content, publishStatus, coverPath, profileDir) {
|
|
21119
21119
|
const metadata = { operation: "article", driver: this.driver, publishStatus, profileDir };
|
|
21120
21120
|
const title = content.title ?? "Untitled";
|
|
21121
|
+
await page.evaluate(ensureArticleDraftScript());
|
|
21122
|
+
const editorUrl = page.url();
|
|
21123
|
+
const toastsBefore = await this.readToasts(page);
|
|
21121
21124
|
const titleSelector = await resolveSelector(page, WEIBO_ARTICLE_SELECTORS.title, {
|
|
21122
21125
|
timeoutMs: this.stepTimeoutMs,
|
|
21123
21126
|
label: "article title input"
|
|
@@ -21129,12 +21132,19 @@ class WeiboBrowserTransport {
|
|
|
21129
21132
|
});
|
|
21130
21133
|
await pasteHtml(page, bodySelector, markdownToHtml(content.body), { timeoutMs: this.stepTimeoutMs });
|
|
21131
21134
|
if (coverPath !== undefined) {
|
|
21135
|
+
const baseline = await this.countImages(page, bodySelector);
|
|
21136
|
+
const preAttached = (await trySelectors(page, WEIBO_ARTICLE_SELECTORS.fileInput, { visible: false })).found;
|
|
21137
|
+
if (!preAttached) {
|
|
21138
|
+
await page.evaluate(openCoverLibraryScript());
|
|
21139
|
+
}
|
|
21132
21140
|
const fileInputSelector = await resolveSelector(page, WEIBO_ARTICLE_SELECTORS.fileInput, {
|
|
21133
21141
|
timeoutMs: this.stepTimeoutMs,
|
|
21134
21142
|
label: "article cover file input"
|
|
21135
21143
|
});
|
|
21136
|
-
const baseline = await this.countImages(page, bodySelector);
|
|
21137
21144
|
await uploadFile(page, fileInputSelector, coverPath, { timeoutMs: this.stepTimeoutMs });
|
|
21145
|
+
if (!preAttached) {
|
|
21146
|
+
await page.evaluate(confirmCoverModalScript());
|
|
21147
|
+
}
|
|
21138
21148
|
const attached = await expectOutcome(() => this.probeCoverAttached(page, bodySelector, baseline), {
|
|
21139
21149
|
timeoutMs: this.stepTimeoutMs,
|
|
21140
21150
|
label: "weibo article cover attachment"
|
|
@@ -21148,8 +21158,6 @@ class WeiboBrowserTransport {
|
|
|
21148
21158
|
const bodyReadback = await this.requireReadback(page, bodySelector, content.body, "article body readback");
|
|
21149
21159
|
if (!bodyReadback.ok)
|
|
21150
21160
|
return fail(bodyReadback.error);
|
|
21151
|
-
const editorUrl = page.url();
|
|
21152
|
-
const toastsBefore = await this.readToasts(page);
|
|
21153
21161
|
if (publishStatus === "draft") {
|
|
21154
21162
|
const saveSelector = await resolveSelector(page, WEIBO_ARTICLE_SELECTORS.saveDraft, {
|
|
21155
21163
|
timeoutMs: this.stepTimeoutMs,
|
|
@@ -21255,7 +21263,10 @@ class WeiboBrowserTransport {
|
|
|
21255
21263
|
}
|
|
21256
21264
|
async probeCoverAttached(page, selector, baseline) {
|
|
21257
21265
|
const count = await this.countImages(page, selector);
|
|
21258
|
-
|
|
21266
|
+
if (count > baseline)
|
|
21267
|
+
return `cover-attached (${count} images)`;
|
|
21268
|
+
const preview = Number(await page.evaluate(`() => document.querySelectorAll('section:not(:has(.n-modal)) .cover-preview, .cover-preview, img.cover-img').length`));
|
|
21269
|
+
return Number.isFinite(preview) && preview > 0 ? `cover-attached (cover-preview x${preview})` : null;
|
|
21259
21270
|
}
|
|
21260
21271
|
async readToasts(page) {
|
|
21261
21272
|
return parseStringArray(await page.evaluate(toastTextsScript()));
|
|
@@ -21277,7 +21288,10 @@ class WeiboBrowserTransport {
|
|
|
21277
21288
|
async probeFreshToast(page, toastsBefore) {
|
|
21278
21289
|
const before = new Set(toastsBefore);
|
|
21279
21290
|
const now = await this.readToasts(page);
|
|
21280
|
-
|
|
21291
|
+
const hit = now.find((text) => !before.has(text) && /\u4FDD\u5B58|\u8349\u7A3F|\u6210\u529F|\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}/.test(text));
|
|
21292
|
+
if (hit === undefined)
|
|
21293
|
+
return null;
|
|
21294
|
+
return /#\/draft\/\d+/.test(page.url()) ? page.url() : hit;
|
|
21281
21295
|
}
|
|
21282
21296
|
async probeArticleLive(page) {
|
|
21283
21297
|
return liveArticleUrl(page.url());
|
|
@@ -21400,12 +21414,104 @@ function coverCountScript(selector) {
|
|
|
21400
21414
|
return `(() => { // kk-weibo-cover
|
|
21401
21415
|
const anchor = document.querySelector(${JSON.stringify(selector)});
|
|
21402
21416
|
const scope = anchor === null ? document.body : anchor.closest('form, section, article, main') || document.body;
|
|
21403
|
-
return String(scope.querySelectorAll('img[src]').length);
|
|
21417
|
+
return String(Array.from(scope.querySelectorAll('img[src]')).filter((el) => !el.closest('.n-modal')).length);
|
|
21418
|
+
})()`;
|
|
21419
|
+
}
|
|
21420
|
+
function ensureArticleDraftScript() {
|
|
21421
|
+
return `(async () => { // kk-weibo-draft-init
|
|
21422
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
21423
|
+
const spin = () => document.querySelectorAll('.wb-editor-spin').length > 0;
|
|
21424
|
+
const titleEl = document.querySelector('input[placeholder*="\u6807\u9898"], textarea[placeholder*="\u6807\u9898"]');
|
|
21425
|
+
const titleVal = titleEl && titleEl.value ? String(titleEl.value).trim() : '';
|
|
21426
|
+
const hasCover = document.querySelectorAll('.cover-preview, img.cover-img').length > 0;
|
|
21427
|
+
if (spin() || titleVal.length > 0 || hasCover) {
|
|
21428
|
+
for (const btn of document.querySelectorAll('button')) {
|
|
21429
|
+
const t = (btn.textContent || '').trim();
|
|
21430
|
+
if (t === '\u5199\u6587\u7AE0' || t.includes('\u521B\u4F5C\u4E00\u7BC7\u65B0\u6587\u7AE0')) {
|
|
21431
|
+
btn.click();
|
|
21432
|
+
break;
|
|
21433
|
+
}
|
|
21434
|
+
}
|
|
21435
|
+
await sleep(600);
|
|
21436
|
+
for (let i = 0; i < 15 && spin(); i += 1) await sleep(200);
|
|
21437
|
+
}
|
|
21438
|
+
return true;
|
|
21439
|
+
})()`;
|
|
21440
|
+
}
|
|
21441
|
+
function openCoverLibraryScript() {
|
|
21442
|
+
return `(async () => { // kk-weibo-open-cover
|
|
21443
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
21444
|
+
let coverEmpty = null;
|
|
21445
|
+
for (let i = 0; i < 15; i += 1) {
|
|
21446
|
+
coverEmpty = document.querySelector('div.cover-empty, [class*="cover-empty"]');
|
|
21447
|
+
if (coverEmpty) break;
|
|
21448
|
+
await sleep(200);
|
|
21449
|
+
}
|
|
21450
|
+
if (coverEmpty instanceof HTMLElement) coverEmpty.click();
|
|
21451
|
+
for (let i = 0; i < 15 && !document.querySelector('.n-modal'); i += 1) await sleep(200);
|
|
21452
|
+
for (const tab of document.querySelectorAll('.n-modal .n-tabs-tab')) {
|
|
21453
|
+
if ((tab.textContent || '').trim() === '\u56FE\u7247\u5E93' && tab instanceof HTMLElement) {
|
|
21454
|
+
tab.click();
|
|
21455
|
+
}
|
|
21456
|
+
}
|
|
21457
|
+
for (let i = 0; i < 30 && !document.querySelector('input[type="file"]'); i += 1) await sleep(200);
|
|
21458
|
+
// The \u56FE\u7247\u5E93 tab lazy-loads (upload button + items mount ~5-8s after the tab click; the fixed
|
|
21459
|
+
// 3s window could race it, and a raced tab leaves fileInputs=0 \u2192 no cover attachment).
|
|
21460
|
+
for (let i = 0; i < 40; i += 1) {
|
|
21461
|
+
if (document.querySelector('.n-modal img, .n-modal input[type="file"]')) break;
|
|
21462
|
+
await sleep(250);
|
|
21463
|
+
}
|
|
21464
|
+
return true;
|
|
21465
|
+
})()`;
|
|
21466
|
+
}
|
|
21467
|
+
function confirmCoverModalScript() {
|
|
21468
|
+
return `(async () => { // kk-weibo-confirm-cover
|
|
21469
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
21470
|
+
if (!document.querySelector('.n-modal')) return true;
|
|
21471
|
+
await sleep(1500);
|
|
21472
|
+
let item = null;
|
|
21473
|
+
for (let i = 0; i < 25; i += 1) {
|
|
21474
|
+
item = document.querySelector('.n-modal .image-item, .n-modal img[src]');
|
|
21475
|
+
if (item) break;
|
|
21476
|
+
await sleep(200);
|
|
21477
|
+
}
|
|
21478
|
+
if (item instanceof HTMLElement && !item.classList.contains('is-selected')) {
|
|
21479
|
+
item.click();
|
|
21480
|
+
}
|
|
21481
|
+
for (let i = 0; i < 15; i += 1) {
|
|
21482
|
+
const nextBtn = Array.from(document.querySelectorAll('.n-modal button')).find(
|
|
21483
|
+
(b) => (b.textContent || '').trim() === '\u4E0B\u4E00\u6B65' && !b.disabled,
|
|
21484
|
+
);
|
|
21485
|
+
if (nextBtn) {
|
|
21486
|
+
nextBtn.click();
|
|
21487
|
+
break;
|
|
21488
|
+
}
|
|
21489
|
+
await sleep(200);
|
|
21490
|
+
}
|
|
21491
|
+
// The \u4E0B\u4E00\u6B65 opens the \u56FE\u7247\u88C1\u526A crop modal (\u4E0A\u4E00\u6B65/\u786E\u5B9A), which takes several seconds to
|
|
21492
|
+
// render \u2014 poll for it before clicking, then wait for the whole modal chain to close.
|
|
21493
|
+
for (let i = 0; i < 40 && !Array.from(document.querySelectorAll('.n-modal button')).find((b) => (b.textContent || '').trim() === '\u786E\u5B9A'); i += 1) {
|
|
21494
|
+
await sleep(250);
|
|
21495
|
+
}
|
|
21496
|
+
for (let i = 0; i < 15; i += 1) {
|
|
21497
|
+
const okBtn = Array.from(document.querySelectorAll('.n-modal button')).find(
|
|
21498
|
+
(b) => (b.textContent || '').trim() === '\u786E\u5B9A' && !b.disabled,
|
|
21499
|
+
);
|
|
21500
|
+
if (okBtn) {
|
|
21501
|
+
okBtn.click();
|
|
21502
|
+
break;
|
|
21503
|
+
}
|
|
21504
|
+
await sleep(200);
|
|
21505
|
+
}
|
|
21506
|
+
for (let i = 0; i < 25 && document.querySelector('.n-modal'); i += 1) {
|
|
21507
|
+
await sleep(200);
|
|
21508
|
+
}
|
|
21509
|
+
return true;
|
|
21404
21510
|
})()`;
|
|
21405
21511
|
}
|
|
21406
21512
|
function toastTextsScript() {
|
|
21407
21513
|
return `(() => { // kk-weibo-toasts
|
|
21408
|
-
const nodes = document.querySelectorAll('[class*="toast" i], [class*="Toast"], [class*="message" i], [role="alert"]');
|
|
21514
|
+
const nodes = document.querySelectorAll('[class*="toast" i], [class*="Toast"], [class*="message" i], [role="alert"], .list-item');
|
|
21409
21515
|
const texts = [];
|
|
21410
21516
|
for (const node of nodes) {
|
|
21411
21517
|
const text = (node.textContent || '').trim();
|
|
@@ -143,7 +143,7 @@ function normalizeText(value: string): string {
|
|
|
143
143
|
}
|
|
144
144
|
|
|
145
145
|
function excerpt(text: string): string {
|
|
146
|
-
return normalizeText(text).slice(0, EXCERPT_CHARS);
|
|
146
|
+
return normalizeText(text.replace(/^[#>*`\-\s]+/gm, '')).slice(0, EXCERPT_CHARS);
|
|
147
147
|
}
|
|
148
148
|
|
|
149
149
|
/**
|
|
@@ -368,6 +368,10 @@ export class WeiboBrowserTransport implements PublishTransport {
|
|
|
368
368
|
const metadata = { operation: 'article', driver: this.driver, publishStatus, profileDir };
|
|
369
369
|
const title = content.title ?? 'Untitled';
|
|
370
370
|
|
|
371
|
+
await page.evaluate(ensureArticleDraftScript());
|
|
372
|
+
const editorUrl = page.url();
|
|
373
|
+
const toastsBefore = await this.readToasts(page);
|
|
374
|
+
|
|
371
375
|
const titleSelector = await resolveSelector(page, WEIBO_ARTICLE_SELECTORS.title, {
|
|
372
376
|
timeoutMs: this.stepTimeoutMs,
|
|
373
377
|
label: 'article title input',
|
|
@@ -382,17 +386,30 @@ export class WeiboBrowserTransport implements PublishTransport {
|
|
|
382
386
|
await pasteHtml(page, bodySelector, markdownToHtml(content.body), { timeoutMs: this.stepTimeoutMs });
|
|
383
387
|
|
|
384
388
|
if (coverPath !== undefined) {
|
|
389
|
+
const baseline = await this.countImages(page, bodySelector);
|
|
390
|
+
const preAttached = (await trySelectors(page, WEIBO_ARTICLE_SELECTORS.fileInput, { visible: false })).found;
|
|
391
|
+
if (!preAttached) {
|
|
392
|
+
await page.evaluate(openCoverLibraryScript());
|
|
393
|
+
}
|
|
385
394
|
const fileInputSelector = await resolveSelector(page, WEIBO_ARTICLE_SELECTORS.fileInput, {
|
|
386
395
|
timeoutMs: this.stepTimeoutMs,
|
|
387
396
|
label: 'article cover file input',
|
|
388
397
|
});
|
|
389
|
-
const baseline = await this.countImages(page, bodySelector);
|
|
390
398
|
await uploadFile(page, fileInputSelector, coverPath, { timeoutMs: this.stepTimeoutMs });
|
|
399
|
+
if (!preAttached) {
|
|
400
|
+
await page.evaluate(confirmCoverModalScript());
|
|
401
|
+
}
|
|
391
402
|
// A requested cover needs a fresh observed attachment, not a successful upload call.
|
|
392
|
-
const attached = await expectOutcome(
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
403
|
+
const attached = await expectOutcome(
|
|
404
|
+
() =>
|
|
405
|
+
// Cover settles in the v5 publish-settings panel (.cover-preview) — outside the
|
|
406
|
+
// body editor scope the body-image count probes, so accept the panel preview too.
|
|
407
|
+
this.probeCoverAttached(page, bodySelector, baseline),
|
|
408
|
+
{
|
|
409
|
+
timeoutMs: this.stepTimeoutMs,
|
|
410
|
+
label: 'weibo article cover attachment',
|
|
411
|
+
},
|
|
412
|
+
);
|
|
396
413
|
if (!attached.ok) return fail(attached.error);
|
|
397
414
|
}
|
|
398
415
|
|
|
@@ -403,9 +420,6 @@ export class WeiboBrowserTransport implements PublishTransport {
|
|
|
403
420
|
const bodyReadback = await this.requireReadback(page, bodySelector, content.body, 'article body readback');
|
|
404
421
|
if (!bodyReadback.ok) return fail(bodyReadback.error);
|
|
405
422
|
|
|
406
|
-
const editorUrl = page.url();
|
|
407
|
-
const toastsBefore = await this.readToasts(page);
|
|
408
|
-
|
|
409
423
|
if (publishStatus === 'draft') {
|
|
410
424
|
const saveSelector = await resolveSelector(page, WEIBO_ARTICLE_SELECTORS.saveDraft, {
|
|
411
425
|
timeoutMs: this.stepTimeoutMs,
|
|
@@ -555,7 +569,15 @@ export class WeiboBrowserTransport implements PublishTransport {
|
|
|
555
569
|
|
|
556
570
|
private async probeCoverAttached(page: Page, selector: string, baseline: number): Promise<string | null> {
|
|
557
571
|
const count = await this.countImages(page, selector);
|
|
558
|
-
|
|
572
|
+
if (count > baseline) return `cover-attached (${count} images)`;
|
|
573
|
+
// The v5 editor routes a modal-picked cover into the publish-settings panel
|
|
574
|
+
// (.cover-preview / img.cover-img) — outside the body scope, so the body count never moves.
|
|
575
|
+
const preview = Number(
|
|
576
|
+
await page.evaluate(
|
|
577
|
+
`() => document.querySelectorAll('section:not(:has(.n-modal)) .cover-preview, .cover-preview, img.cover-img').length`,
|
|
578
|
+
),
|
|
579
|
+
);
|
|
580
|
+
return Number.isFinite(preview) && preview > 0 ? `cover-attached (cover-preview x${preview})` : null;
|
|
559
581
|
}
|
|
560
582
|
|
|
561
583
|
/** Visible toast texts before a save, so an already-visible toast cannot satisfy the draft rail. */
|
|
@@ -590,11 +612,15 @@ export class WeiboBrowserTransport implements PublishTransport {
|
|
|
590
612
|
return parts.join('; ');
|
|
591
613
|
}
|
|
592
614
|
|
|
593
|
-
/** A toast carrying a save/success
|
|
615
|
+
/** A toast or v5 draft-list item carrying a save/success/timestamp signal not on screen before the flow. */
|
|
594
616
|
private async probeFreshToast(page: Page, toastsBefore: readonly string[]): Promise<string | null> {
|
|
595
617
|
const before = new Set(toastsBefore);
|
|
596
618
|
const now = await this.readToasts(page);
|
|
597
|
-
|
|
619
|
+
const hit = now.find(
|
|
620
|
+
(text) => !before.has(text) && /保存|草稿|成功|\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}/.test(text),
|
|
621
|
+
);
|
|
622
|
+
if (hit === undefined) return null;
|
|
623
|
+
return /#\/draft\/\d+/.test(page.url()) ? page.url() : hit;
|
|
598
624
|
}
|
|
599
625
|
|
|
600
626
|
/** Live article evidence: the browser left the editor for a published article URL. */
|
|
@@ -751,19 +777,117 @@ function readbackScript(selector: string): string {
|
|
|
751
777
|
})()`;
|
|
752
778
|
}
|
|
753
779
|
|
|
754
|
-
/** Counts rendered images in the editor's nearest container, for the cover baseline. */
|
|
780
|
+
/** Counts rendered images in the editor's nearest container (excluding modal portals), for the cover baseline. */
|
|
755
781
|
function coverCountScript(selector: string): string {
|
|
756
782
|
return `(() => { // kk-weibo-cover
|
|
757
783
|
const anchor = document.querySelector(${JSON.stringify(selector)});
|
|
758
784
|
const scope = anchor === null ? document.body : anchor.closest('form, section, article, main') || document.body;
|
|
759
|
-
return String(scope.querySelectorAll('img[src]').length);
|
|
785
|
+
return String(Array.from(scope.querySelectorAll('img[src]')).filter((el) => !el.closest('.n-modal')).length);
|
|
786
|
+
})()`;
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
/** Ensures the v5 article editor has an active, blank draft pane before filling title/body. */
|
|
790
|
+
function ensureArticleDraftScript(): string {
|
|
791
|
+
return `(async () => { // kk-weibo-draft-init
|
|
792
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
793
|
+
const spin = () => document.querySelectorAll('.wb-editor-spin').length > 0;
|
|
794
|
+
const titleEl = document.querySelector('input[placeholder*="标题"], textarea[placeholder*="标题"]');
|
|
795
|
+
const titleVal = titleEl && titleEl.value ? String(titleEl.value).trim() : '';
|
|
796
|
+
const hasCover = document.querySelectorAll('.cover-preview, img.cover-img').length > 0;
|
|
797
|
+
if (spin() || titleVal.length > 0 || hasCover) {
|
|
798
|
+
for (const btn of document.querySelectorAll('button')) {
|
|
799
|
+
const t = (btn.textContent || '').trim();
|
|
800
|
+
if (t === '写文章' || t.includes('创作一篇新文章')) {
|
|
801
|
+
btn.click();
|
|
802
|
+
break;
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
await sleep(600);
|
|
806
|
+
for (let i = 0; i < 15 && spin(); i += 1) await sleep(200);
|
|
807
|
+
}
|
|
808
|
+
return true;
|
|
809
|
+
})()`;
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
/** Opens the v5 cover modal (`div.cover-empty`) and switches to the `图片库` tab so `input[type="file"]` mounts. */
|
|
813
|
+
function openCoverLibraryScript(): string {
|
|
814
|
+
return `(async () => { // kk-weibo-open-cover
|
|
815
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
816
|
+
let coverEmpty = null;
|
|
817
|
+
for (let i = 0; i < 15; i += 1) {
|
|
818
|
+
coverEmpty = document.querySelector('div.cover-empty, [class*="cover-empty"]');
|
|
819
|
+
if (coverEmpty) break;
|
|
820
|
+
await sleep(200);
|
|
821
|
+
}
|
|
822
|
+
if (coverEmpty instanceof HTMLElement) coverEmpty.click();
|
|
823
|
+
for (let i = 0; i < 15 && !document.querySelector('.n-modal'); i += 1) await sleep(200);
|
|
824
|
+
for (const tab of document.querySelectorAll('.n-modal .n-tabs-tab')) {
|
|
825
|
+
if ((tab.textContent || '').trim() === '图片库' && tab instanceof HTMLElement) {
|
|
826
|
+
tab.click();
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
for (let i = 0; i < 30 && !document.querySelector('input[type="file"]'); i += 1) await sleep(200);
|
|
830
|
+
// The 图片库 tab lazy-loads (upload button + items mount ~5-8s after the tab click; the fixed
|
|
831
|
+
// 3s window could race it, and a raced tab leaves fileInputs=0 → no cover attachment).
|
|
832
|
+
for (let i = 0; i < 40; i += 1) {
|
|
833
|
+
if (document.querySelector('.n-modal img, .n-modal input[type="file"]')) break;
|
|
834
|
+
await sleep(250);
|
|
835
|
+
}
|
|
836
|
+
return true;
|
|
837
|
+
})()`;
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
/** Selects the uploaded image in the v5 `.n-modal` library, advances through crop (`下一步` → `确定`), and waits for the modal to close. */
|
|
841
|
+
function confirmCoverModalScript(): string {
|
|
842
|
+
return `(async () => { // kk-weibo-confirm-cover
|
|
843
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
844
|
+
if (!document.querySelector('.n-modal')) return true;
|
|
845
|
+
await sleep(1500);
|
|
846
|
+
let item = null;
|
|
847
|
+
for (let i = 0; i < 25; i += 1) {
|
|
848
|
+
item = document.querySelector('.n-modal .image-item, .n-modal img[src]');
|
|
849
|
+
if (item) break;
|
|
850
|
+
await sleep(200);
|
|
851
|
+
}
|
|
852
|
+
if (item instanceof HTMLElement && !item.classList.contains('is-selected')) {
|
|
853
|
+
item.click();
|
|
854
|
+
}
|
|
855
|
+
for (let i = 0; i < 15; i += 1) {
|
|
856
|
+
const nextBtn = Array.from(document.querySelectorAll('.n-modal button')).find(
|
|
857
|
+
(b) => (b.textContent || '').trim() === '下一步' && !b.disabled,
|
|
858
|
+
);
|
|
859
|
+
if (nextBtn) {
|
|
860
|
+
nextBtn.click();
|
|
861
|
+
break;
|
|
862
|
+
}
|
|
863
|
+
await sleep(200);
|
|
864
|
+
}
|
|
865
|
+
// The 下一步 opens the 图片裁剪 crop modal (上一步/确定), which takes several seconds to
|
|
866
|
+
// render — poll for it before clicking, then wait for the whole modal chain to close.
|
|
867
|
+
for (let i = 0; i < 40 && !Array.from(document.querySelectorAll('.n-modal button')).find((b) => (b.textContent || '').trim() === '确定'); i += 1) {
|
|
868
|
+
await sleep(250);
|
|
869
|
+
}
|
|
870
|
+
for (let i = 0; i < 15; i += 1) {
|
|
871
|
+
const okBtn = Array.from(document.querySelectorAll('.n-modal button')).find(
|
|
872
|
+
(b) => (b.textContent || '').trim() === '确定' && !b.disabled,
|
|
873
|
+
);
|
|
874
|
+
if (okBtn) {
|
|
875
|
+
okBtn.click();
|
|
876
|
+
break;
|
|
877
|
+
}
|
|
878
|
+
await sleep(200);
|
|
879
|
+
}
|
|
880
|
+
for (let i = 0; i < 25 && document.querySelector('.n-modal'); i += 1) {
|
|
881
|
+
await sleep(200);
|
|
882
|
+
}
|
|
883
|
+
return true;
|
|
760
884
|
})()`;
|
|
761
885
|
}
|
|
762
886
|
|
|
763
|
-
/** Visible toast texts, used as the pre-save baseline and as the fresh-signal read. */
|
|
887
|
+
/** Visible toast texts and v5 draft sidebar items, used as the pre-save baseline and as the fresh-signal read. */
|
|
764
888
|
function toastTextsScript(): string {
|
|
765
889
|
return `(() => { // kk-weibo-toasts
|
|
766
|
-
const nodes = document.querySelectorAll('[class*="toast" i], [class*="Toast"], [class*="message" i], [role="alert"]');
|
|
890
|
+
const nodes = document.querySelectorAll('[class*="toast" i], [class*="Toast"], [class*="message" i], [role="alert"], .list-item');
|
|
767
891
|
const texts = [];
|
|
768
892
|
for (const node of nodes) {
|
|
769
893
|
const text = (node.textContent || '').trim();
|
|
@@ -21342,6 +21342,7 @@ function wrapPlaywrightPage(raw, shadow) {
|
|
|
21342
21342
|
isVisible: (options) => loc.isVisible(options),
|
|
21343
21343
|
fill: (text, options) => loc.fill(text, options),
|
|
21344
21344
|
click: (options) => loc.click(options),
|
|
21345
|
+
boundingBox: async () => (await onScreen(selector)).at(-1)?.box ?? await loc.boundingBox?.() ?? null,
|
|
21345
21346
|
type: (text, options) => loc.pressSequentially(text, options),
|
|
21346
21347
|
textContent: () => loc.textContent()
|
|
21347
21348
|
};
|
|
@@ -21360,7 +21361,16 @@ function wrapPlaywrightPage(raw, shadow) {
|
|
|
21360
21361
|
return true;
|
|
21361
21362
|
},
|
|
21362
21363
|
findShadowControl: async (labels) => shadow ? shadow.findControl(labels) : null,
|
|
21363
|
-
clickAt: (x, y) => pw.mouse.click(x, y)
|
|
21364
|
+
clickAt: (x, y) => pw.mouse.click(x, y),
|
|
21365
|
+
waitForEvent: (event, options) => {
|
|
21366
|
+
if (typeof pw.waitForEvent !== "function") {
|
|
21367
|
+
return Promise.reject(new Error(`the page seam does not expose a '${event}' event source`));
|
|
21368
|
+
}
|
|
21369
|
+
return pw.waitForEvent(event, options);
|
|
21370
|
+
},
|
|
21371
|
+
mouse: {
|
|
21372
|
+
click: (x, y) => pw.mouse.click(x, y)
|
|
21373
|
+
}
|
|
21364
21374
|
};
|
|
21365
21375
|
}
|
|
21366
21376
|
async function findSelector(page, selectors, what, timeoutMs = STEP_TIMEOUT_MS) {
|
|
@@ -21412,8 +21422,9 @@ var ARTICLE_LAYOUT = ['button:has-text("\u4E00\u952E\u6392\u7248")', "div.ailayo
|
|
|
21412
21422
|
var PODCAST_OPEN = ["text=\u4E0A\u4F20\u97F3\u9891", 'button:has-text("\u4E0A\u4F20\u97F3\u9891")'];
|
|
21413
21423
|
var PODCAST_MODAL = ["div.creator-modal-sty", ".d-modal.d-modal-centered", ".d-modal"];
|
|
21414
21424
|
var PODCAST_AUDIO_INPUT = ['input[type="file"][accept*=".mp3"]', 'input[type="file"][accept*=".wav"]'];
|
|
21415
|
-
var PODCAST_COVER_INPUT = ['input[type="file"][accept*=".jpg"]', 'input[type="file"][accept*=".png"]'];
|
|
21416
21425
|
var PODCAST_MODAL_ADVANCE = ["\u53BB\u53D1\u5E03"];
|
|
21426
|
+
var PODCAST_COVER_ZONE = '.d-modal div:has-text("\u8BBE\u7F6E\u5C01\u9762")';
|
|
21427
|
+
var PODCAST_UPLOAD_ACK = "\u4E0A\u4F20\u6210\u529F";
|
|
21417
21428
|
var DRAFT_LABELS = ["\u6682\u5B58\u79BB\u5F00", "\u5B58\u8349\u7A3F", "\u4FDD\u5B58\u8349\u7A3F"];
|
|
21418
21429
|
var PUBLISH_LABELS = ["\u53D1\u5E03"];
|
|
21419
21430
|
var PUBLISH_CONFIRM = ["\u53D1\u5E03\u6210\u529F", "\u5DF2\u53D1\u5E03"];
|
|
@@ -21589,18 +21600,67 @@ async function composeArticle(deps) {
|
|
|
21589
21600
|
async function isPodcastModalOpen(page) {
|
|
21590
21601
|
return trySelectors(page, PODCAST_MODAL).then((found) => found.found).catch(() => false);
|
|
21591
21602
|
}
|
|
21603
|
+
async function clickPodcastModalDomFallback(page) {
|
|
21604
|
+
const res = await page.evaluate(`(() => {
|
|
21605
|
+
const modals = Array.from(document.querySelectorAll('.d-modal, div.creator-modal-sty'));
|
|
21606
|
+
const buttons = Array.from(document.querySelectorAll('.d-modal button, div.creator-modal-sty button, .d-modal-mask button'));
|
|
21607
|
+
const confirmLabels = ['\u786E\u5B9A', '\u5B8C\u6210', '\u786E\u8BA4', '\u4FDD\u5B58'];
|
|
21608
|
+
const cropBtn = buttons.find((b) => {
|
|
21609
|
+
const t = (b.textContent || '').trim();
|
|
21610
|
+
return confirmLabels.includes(t) && !b.classList.contains('cancel-btn') && !b.dataset.kkClicked;
|
|
21611
|
+
}) || (modals.length > 1 ? Array.from(modals[modals.length - 1].querySelectorAll('button')).find((b) => !b.classList.contains('cancel-btn') && b.classList.contains('bg-red') && !(b.textContent || '').includes('\u53BB\u53D1\u5E03') && !b.dataset.kkClicked) : null);
|
|
21612
|
+
if (cropBtn && typeof cropBtn.click === 'function') {
|
|
21613
|
+
cropBtn.dataset.kkClicked = '1';
|
|
21614
|
+
cropBtn.click();
|
|
21615
|
+
return JSON.stringify({ modals: modals.length, clickedCrop: true, clickedAdvance: false });
|
|
21616
|
+
}
|
|
21617
|
+
const advBtns = buttons.filter((b) => (b.textContent || '').includes('\u53BB\u53D1\u5E03') && !b.hasAttribute('disabled') && b.getAttribute('aria-disabled') !== 'true');
|
|
21618
|
+
const adv = advBtns[advBtns.length - 1];
|
|
21619
|
+
let clickedAdvance = false;
|
|
21620
|
+
if (adv && !adv.dataset.kkClicked && typeof adv.click === 'function') {
|
|
21621
|
+
adv.dataset.kkClicked = '1';
|
|
21622
|
+
adv.click();
|
|
21623
|
+
clickedAdvance = true;
|
|
21624
|
+
}
|
|
21625
|
+
return JSON.stringify({
|
|
21626
|
+
modals: modals.length,
|
|
21627
|
+
clickedCrop: false,
|
|
21628
|
+
clickedAdvance,
|
|
21629
|
+
buttons: buttons.map((b) => ({ text: (b.textContent || '').trim(), cls: b.className, disabled: b.hasAttribute('disabled') })),
|
|
21630
|
+
});
|
|
21631
|
+
})()`).catch(() => "");
|
|
21632
|
+
return typeof res === "string" ? res : "";
|
|
21633
|
+
}
|
|
21592
21634
|
async function leavePodcastModal(deps) {
|
|
21593
21635
|
const closed = () => isPodcastModalOpen(deps.page).then((open) => !open);
|
|
21594
|
-
if (await
|
|
21636
|
+
if (!await isPodcastModalOpen(deps.page))
|
|
21637
|
+
return;
|
|
21638
|
+
let usedFallback = false;
|
|
21639
|
+
let lastDiag = "";
|
|
21640
|
+
try {
|
|
21641
|
+
const advance = await clickLightControl(deps.page, PODCAST_MODAL_ADVANCE, Math.min(deps.stepTimeoutMs, 2500), "podcast modal advance");
|
|
21642
|
+
if (!advance.ok) {
|
|
21643
|
+
lastDiag = await clickPodcastModalDomFallback(deps.page);
|
|
21644
|
+
if (!lastDiag.startsWith("{"))
|
|
21645
|
+
throw new Error(advance.error);
|
|
21646
|
+
usedFallback = true;
|
|
21647
|
+
}
|
|
21648
|
+
} catch {
|
|
21649
|
+
lastDiag = await clickPodcastModalDomFallback(deps.page);
|
|
21650
|
+
usedFallback = true;
|
|
21651
|
+
}
|
|
21652
|
+
if (await waitUntil(async () => {
|
|
21653
|
+
if (await closed())
|
|
21654
|
+
return true;
|
|
21655
|
+
if (usedFallback) {
|
|
21656
|
+
lastDiag = await clickPodcastModalDomFallback(deps.page);
|
|
21657
|
+
}
|
|
21658
|
+
return false;
|
|
21659
|
+
}, deps.stepTimeoutMs)) {
|
|
21595
21660
|
return;
|
|
21596
|
-
if (deps.publishStatus === "public") {
|
|
21597
|
-
const advance = await clickLightControl(deps.page, PODCAST_MODAL_ADVANCE, deps.stepTimeoutMs, "podcast modal advance");
|
|
21598
|
-
if (!advance.ok)
|
|
21599
|
-
throw new Error(advance.error);
|
|
21600
|
-
if (await waitUntil(closed, deps.stepTimeoutMs))
|
|
21601
|
-
return;
|
|
21602
21661
|
}
|
|
21603
|
-
|
|
21662
|
+
const diagSuffix = lastDiag ? ` [modal=${lastDiag}]` : "";
|
|
21663
|
+
throw new Error(`podcast modal: the \u53D1\u64AD\u5BA2 modal stayed open ${deps.stepTimeoutMs}ms after the advance click; the measured 2026-11-06 flow moves to the podcast editor page with the closed-shadow '${DRAFT_LABELS[0]}' bar, so a fast re-measure with a fresh probe is the next step, not a retry${diagSuffix}`);
|
|
21604
21664
|
}
|
|
21605
21665
|
async function composePodcast(input, deps) {
|
|
21606
21666
|
await enterMode(deps, input.operation);
|
|
@@ -21609,10 +21669,102 @@ async function composePodcast(input, deps) {
|
|
|
21609
21669
|
await findSelector(deps.page, PODCAST_MODAL, "podcast modal", deps.stepTimeoutMs);
|
|
21610
21670
|
const audioInput = await findAttachedSelector(deps.page, PODCAST_AUDIO_INPUT, "podcast audio input", deps.stepTimeoutMs);
|
|
21611
21671
|
await uploadFile(asPlaywrightPage(deps.page), audioInput, input.audio, { timeoutMs: deps.stepTimeoutMs });
|
|
21612
|
-
const
|
|
21613
|
-
|
|
21672
|
+
const ack = await waitUntil(async () => {
|
|
21673
|
+
try {
|
|
21674
|
+
return await deps.page.locator(`text=${PODCAST_UPLOAD_ACK}`).count() > 0;
|
|
21675
|
+
} catch {
|
|
21676
|
+
return false;
|
|
21677
|
+
}
|
|
21678
|
+
}, deps.stepTimeoutMs);
|
|
21679
|
+
if (!ack) {
|
|
21680
|
+
throw new Error(`podcast modal: the audio card never showed '${PODCAST_UPLOAD_ACK}' within ${deps.stepTimeoutMs}ms of the file assignment; mimic-file upload of ${input.audio.split("/").pop()} did not register`);
|
|
21681
|
+
}
|
|
21682
|
+
await uploadPodcastCover(deps, input.cover);
|
|
21683
|
+
await podcastAdvanceReady(deps);
|
|
21614
21684
|
await leavePodcastModal(deps);
|
|
21615
21685
|
}
|
|
21686
|
+
async function uploadPodcastCover(deps, cover) {
|
|
21687
|
+
const zone = deps.page.locator(PODCAST_COVER_ZONE).first();
|
|
21688
|
+
const box = await zone.boundingBox().catch(() => null);
|
|
21689
|
+
if (!box) {
|
|
21690
|
+
throw new Error(`podcast modal: the cover card ('${PODCAST_COVER_ZONE}') has no bounding box to click`);
|
|
21691
|
+
}
|
|
21692
|
+
if (!deps.page.mouse) {
|
|
21693
|
+
throw new Error("podcast modal: the page seam does not expose a raw mouse for the cover-card coordinate click");
|
|
21694
|
+
}
|
|
21695
|
+
const chooser = deps.page.waitForEvent?.("filechooser", { timeout: 8000 }).catch(() => null);
|
|
21696
|
+
await deps.page.mouse.click(box.x + box.width / 2, box.y + box.height / 2);
|
|
21697
|
+
const pending = Symbol("pending");
|
|
21698
|
+
const quick = await Promise.race([
|
|
21699
|
+
chooser,
|
|
21700
|
+
new Promise((resolve) => setTimeout(() => resolve(pending), 400))
|
|
21701
|
+
]);
|
|
21702
|
+
let diag = "";
|
|
21703
|
+
if (quick === pending) {
|
|
21704
|
+
const fallback = await deps.page.evaluate(`(() => {
|
|
21705
|
+
const modal = document.querySelector('.d-modal') || document.body;
|
|
21706
|
+
const inputs = Array.from(modal.querySelectorAll('input[type="file"]'));
|
|
21707
|
+
const coverInput = inputs.find((i) => !String(i.getAttribute('accept') || '').includes('.mp3'));
|
|
21708
|
+
const all = Array.from(modal.querySelectorAll('*')).filter((el) => (el.textContent || '').includes('\u8BBE\u7F6E\u5C01\u9762'));
|
|
21709
|
+
const leaf = all[all.length - 1];
|
|
21710
|
+
let card = leaf;
|
|
21711
|
+
while (card && card.parentElement && card.parentElement !== modal) {
|
|
21712
|
+
const pt = card.parentElement.textContent || '';
|
|
21713
|
+
if (pt.includes('\u4E0A\u4F20\u6210\u529F') || pt.includes('\u70B9\u51FB\u4E0A\u4F20') || pt.includes('\u53BB\u53D1\u5E03')) break;
|
|
21714
|
+
card = card.parentElement;
|
|
21715
|
+
}
|
|
21716
|
+
let clickedInput = false;
|
|
21717
|
+
if (coverInput && typeof coverInput.click === 'function') {
|
|
21718
|
+
coverInput.click();
|
|
21719
|
+
clickedInput = true;
|
|
21720
|
+
} else {
|
|
21721
|
+
if (card && typeof card.click === 'function') card.click();
|
|
21722
|
+
if (leaf && typeof leaf.click === 'function') leaf.click();
|
|
21723
|
+
}
|
|
21724
|
+
const rect = card ? card.getBoundingClientRect() : null;
|
|
21725
|
+
return JSON.stringify({
|
|
21726
|
+
clickedInput,
|
|
21727
|
+
x: rect ? Math.round(rect.left + rect.width / 2) : null,
|
|
21728
|
+
y: rect ? Math.round(rect.top + rect.height * 0.35) : null,
|
|
21729
|
+
inputs: inputs.map((i) => ({ accept: i.getAttribute('accept'), cls: i.className })),
|
|
21730
|
+
cardTag: card ? card.tagName + '.' + String(card.className) : null,
|
|
21731
|
+
cardRect: rect ? [Math.round(rect.left), Math.round(rect.top), Math.round(rect.width), Math.round(rect.height)] : null,
|
|
21732
|
+
});
|
|
21733
|
+
})()`).catch(() => "");
|
|
21734
|
+
if (typeof fallback === "string" && fallback.startsWith("{")) {
|
|
21735
|
+
diag = ` [modal=${fallback}]`;
|
|
21736
|
+
try {
|
|
21737
|
+
const parsed = JSON.parse(fallback);
|
|
21738
|
+
if (!parsed.clickedInput && typeof parsed.x === "number" && typeof parsed.y === "number") {
|
|
21739
|
+
await deps.page.mouse.click(parsed.x, parsed.y);
|
|
21740
|
+
}
|
|
21741
|
+
} catch {}
|
|
21742
|
+
}
|
|
21743
|
+
}
|
|
21744
|
+
const opened = quick === pending ? await chooser : quick;
|
|
21745
|
+
if (!opened) {
|
|
21746
|
+
throw new Error(`podcast modal: clicking the cover card did not open a native file chooser within 8000ms${diag}`);
|
|
21747
|
+
}
|
|
21748
|
+
await opened.setFiles(cover);
|
|
21749
|
+
}
|
|
21750
|
+
async function podcastAdvanceReady(deps) {
|
|
21751
|
+
if (deps.publishStatus !== "draft") {
|
|
21752
|
+
return;
|
|
21753
|
+
}
|
|
21754
|
+
const enabled = await waitUntil(async () => {
|
|
21755
|
+
try {
|
|
21756
|
+
if (await deps.page.locator('button:has-text("\u53BB\u53D1\u5E03")').count() === 0)
|
|
21757
|
+
return false;
|
|
21758
|
+
const disabled = await deps.page.locator('button:has-text("\u53BB\u53D1\u5E03")[disabled], button:has-text("\u53BB\u53D1\u5E03")[aria-disabled="true"]').count();
|
|
21759
|
+
return disabled === 0;
|
|
21760
|
+
} catch {
|
|
21761
|
+
return false;
|
|
21762
|
+
}
|
|
21763
|
+
}, deps.stepTimeoutMs);
|
|
21764
|
+
if (!enabled) {
|
|
21765
|
+
throw new Error(`podcast modal: '${PODCAST_MODAL_ADVANCE[0]}' stayed disabled \u2014 the cards were not both accepted; check the audio ack '${PODCAST_UPLOAD_ACK}' and retry with the cover chooser path`);
|
|
21766
|
+
}
|
|
21767
|
+
}
|
|
21616
21768
|
async function compose(input, deps) {
|
|
21617
21769
|
switch (input.operation) {
|
|
21618
21770
|
case "\u4E0A\u4F20\u56FE\u6587":
|
|
@@ -20996,6 +20996,7 @@ function wrapPlaywrightPage(raw, shadow) {
|
|
|
20996
20996
|
isVisible: (options) => loc.isVisible(options),
|
|
20997
20997
|
fill: (text, options) => loc.fill(text, options),
|
|
20998
20998
|
click: (options) => loc.click(options),
|
|
20999
|
+
boundingBox: async () => (await onScreen(selector)).at(-1)?.box ?? await loc.boundingBox?.() ?? null,
|
|
20999
21000
|
type: (text, options) => loc.pressSequentially(text, options),
|
|
21000
21001
|
textContent: () => loc.textContent()
|
|
21001
21002
|
};
|
|
@@ -21014,7 +21015,16 @@ function wrapPlaywrightPage(raw, shadow) {
|
|
|
21014
21015
|
return true;
|
|
21015
21016
|
},
|
|
21016
21017
|
findShadowControl: async (labels) => shadow ? shadow.findControl(labels) : null,
|
|
21017
|
-
clickAt: (x, y) => pw.mouse.click(x, y)
|
|
21018
|
+
clickAt: (x, y) => pw.mouse.click(x, y),
|
|
21019
|
+
waitForEvent: (event, options) => {
|
|
21020
|
+
if (typeof pw.waitForEvent !== "function") {
|
|
21021
|
+
return Promise.reject(new Error(`the page seam does not expose a '${event}' event source`));
|
|
21022
|
+
}
|
|
21023
|
+
return pw.waitForEvent(event, options);
|
|
21024
|
+
},
|
|
21025
|
+
mouse: {
|
|
21026
|
+
click: (x, y) => pw.mouse.click(x, y)
|
|
21027
|
+
}
|
|
21018
21028
|
};
|
|
21019
21029
|
}
|
|
21020
21030
|
|