@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
|
@@ -57,8 +57,15 @@ const ARTICLE_LAYOUT = ['button:has-text("一键排版")', 'div.ailayout-btn_wrp
|
|
|
57
57
|
const PODCAST_OPEN = ['text=上传音频', 'button:has-text("上传音频")'];
|
|
58
58
|
const PODCAST_MODAL = ['div.creator-modal-sty', '.d-modal.d-modal-centered', '.d-modal'];
|
|
59
59
|
const PODCAST_AUDIO_INPUT = ['input[type="file"][accept*=".mp3"]', 'input[type="file"][accept*=".wav"]'];
|
|
60
|
-
const PODCAST_COVER_INPUT = ['input[type="file"][accept*=".jpg"]', 'input[type="file"][accept*=".png"]'];
|
|
61
60
|
const PODCAST_MODAL_ADVANCE = ['去发布'];
|
|
61
|
+
/* Measured post-attach flow (2026-11-06 headless probe, 10-min mp3 + 1:1 png): a raw
|
|
62
|
+
* input.files assignment registers the AUDIO (`上传成功` appears) but the COVER card never
|
|
63
|
+
* advances and `去发布` stays `disabled` — only clicking the cover card and handing the file to
|
|
64
|
+
* the native file chooser the site opens takes both. `去发布` is then just the ADVANCE control: the
|
|
65
|
+
* next page is the full podcast editor carrying the closed-shadow bar (`暂存离开`@left, `发布`@right),
|
|
66
|
+
* so a draft run may click it too — it does not publish on its own. */
|
|
67
|
+
const PODCAST_COVER_ZONE = '.d-modal div:has-text("设置封面")';
|
|
68
|
+
const PODCAST_UPLOAD_ACK = '上传成功';
|
|
62
69
|
|
|
63
70
|
/**
|
|
64
71
|
* The `暂存离开` labels, newest first: XHS renamed the control, and the older labels stay as
|
|
@@ -401,32 +408,85 @@ async function isPodcastModalOpen(page: XhsPage): Promise<boolean> {
|
|
|
401
408
|
.catch(() => false);
|
|
402
409
|
}
|
|
403
410
|
|
|
411
|
+
async function clickPodcastModalDomFallback(page: XhsPage): Promise<string> {
|
|
412
|
+
const res = await page
|
|
413
|
+
.evaluate(
|
|
414
|
+
`(() => {
|
|
415
|
+
const modals = Array.from(document.querySelectorAll('.d-modal, div.creator-modal-sty'));
|
|
416
|
+
const buttons = Array.from(document.querySelectorAll('.d-modal button, div.creator-modal-sty button, .d-modal-mask button'));
|
|
417
|
+
const confirmLabels = ['确定', '完成', '确认', '保存'];
|
|
418
|
+
const cropBtn = buttons.find((b) => {
|
|
419
|
+
const t = (b.textContent || '').trim();
|
|
420
|
+
return confirmLabels.includes(t) && !b.classList.contains('cancel-btn') && !b.dataset.kkClicked;
|
|
421
|
+
}) || (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('去发布') && !b.dataset.kkClicked) : null);
|
|
422
|
+
if (cropBtn && typeof cropBtn.click === 'function') {
|
|
423
|
+
cropBtn.dataset.kkClicked = '1';
|
|
424
|
+
cropBtn.click();
|
|
425
|
+
return JSON.stringify({ modals: modals.length, clickedCrop: true, clickedAdvance: false });
|
|
426
|
+
}
|
|
427
|
+
const advBtns = buttons.filter((b) => (b.textContent || '').includes('去发布') && !b.hasAttribute('disabled') && b.getAttribute('aria-disabled') !== 'true');
|
|
428
|
+
const adv = advBtns[advBtns.length - 1];
|
|
429
|
+
let clickedAdvance = false;
|
|
430
|
+
if (adv && !adv.dataset.kkClicked && typeof adv.click === 'function') {
|
|
431
|
+
adv.dataset.kkClicked = '1';
|
|
432
|
+
adv.click();
|
|
433
|
+
clickedAdvance = true;
|
|
434
|
+
}
|
|
435
|
+
return JSON.stringify({
|
|
436
|
+
modals: modals.length,
|
|
437
|
+
clickedCrop: false,
|
|
438
|
+
clickedAdvance,
|
|
439
|
+
buttons: buttons.map((b) => ({ text: (b.textContent || '').trim(), cls: b.className, disabled: b.hasAttribute('disabled') })),
|
|
440
|
+
});
|
|
441
|
+
})()`,
|
|
442
|
+
)
|
|
443
|
+
.catch(() => '');
|
|
444
|
+
return typeof res === 'string' ? res : '';
|
|
445
|
+
}
|
|
446
|
+
|
|
404
447
|
/**
|
|
405
|
-
* Leave the 发播客 modal.
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
*
|
|
409
|
-
*
|
|
410
|
-
*
|
|
411
|
-
* modal open), and an attended walk under ADR-025 is what would measure it. So no draft-save selector
|
|
412
|
-
* is invented here: the run stops before the publish-adjacent control and says so, which is the
|
|
413
|
-
* failure the 0236 result carries (a `ok:false` entry that never fails the visit).
|
|
448
|
+
* Leave the 发播客 modal. 0236 R3 is now measured (2026-11-06 headless probe): the modal's only
|
|
449
|
+
* controls are `取消` and the light-DOM `去发布`, and `去发布` does not publish — it opens the full
|
|
450
|
+
* podcast editor whose action bar is the same closed-shadow `暂存离开`/`发布` pair every other mode
|
|
451
|
+
* uses, so BOTH publish statuses take the same advance click and the draft decision moves to the
|
|
452
|
+
* editor's action bar (MODE_ACTION_PATHS['发播客'] already routes `shadow`). Draft saving
|
|
453
|
+
* waits for the modal to close by itself first — only that timed-dead path names what it saw.
|
|
414
454
|
*/
|
|
415
455
|
async function leavePodcastModal(deps: ModeRunDeps): Promise<void> {
|
|
416
456
|
const closed = () => isPodcastModalOpen(deps.page).then((open) => !open);
|
|
417
|
-
if (await
|
|
418
|
-
|
|
457
|
+
if (!(await isPodcastModalOpen(deps.page))) return;
|
|
458
|
+
let usedFallback = false;
|
|
459
|
+
let lastDiag = '';
|
|
460
|
+
try {
|
|
419
461
|
const advance = await clickLightControl(
|
|
420
462
|
deps.page,
|
|
421
463
|
PODCAST_MODAL_ADVANCE,
|
|
422
|
-
deps.stepTimeoutMs,
|
|
464
|
+
Math.min(deps.stepTimeoutMs, 2_500),
|
|
423
465
|
'podcast modal advance',
|
|
424
466
|
);
|
|
425
|
-
if (!advance.ok)
|
|
426
|
-
|
|
467
|
+
if (!advance.ok) {
|
|
468
|
+
lastDiag = await clickPodcastModalDomFallback(deps.page);
|
|
469
|
+
if (!lastDiag.startsWith('{')) throw new Error(advance.error);
|
|
470
|
+
usedFallback = true;
|
|
471
|
+
}
|
|
472
|
+
} catch {
|
|
473
|
+
lastDiag = await clickPodcastModalDomFallback(deps.page);
|
|
474
|
+
usedFallback = true;
|
|
475
|
+
}
|
|
476
|
+
if (
|
|
477
|
+
await waitUntil(async () => {
|
|
478
|
+
if (await closed()) return true;
|
|
479
|
+
if (usedFallback) {
|
|
480
|
+
lastDiag = await clickPodcastModalDomFallback(deps.page);
|
|
481
|
+
}
|
|
482
|
+
return false;
|
|
483
|
+
}, deps.stepTimeoutMs)
|
|
484
|
+
) {
|
|
485
|
+
return;
|
|
427
486
|
}
|
|
487
|
+
const diagSuffix = lastDiag ? ` [modal=${lastDiag}]` : '';
|
|
428
488
|
throw new Error(
|
|
429
|
-
`podcast modal: the 发播客 modal
|
|
489
|
+
`podcast modal: the 发播客 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}`,
|
|
430
490
|
);
|
|
431
491
|
}
|
|
432
492
|
|
|
@@ -445,16 +505,127 @@ async function composePodcast(
|
|
|
445
505
|
deps.stepTimeoutMs,
|
|
446
506
|
);
|
|
447
507
|
await uploadFile(asPlaywrightPage(deps.page), audioInput, input.audio, { timeoutMs: deps.stepTimeoutMs });
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
508
|
+
// The audio card's acknowledgement is the registration signal: no ack in the window means the
|
|
509
|
+
// assignment did not take, and the cover chooser would then find `去发布` disabled and stall.
|
|
510
|
+
const ack = await waitUntil(async () => {
|
|
511
|
+
try {
|
|
512
|
+
return (await deps.page.locator(`text=${PODCAST_UPLOAD_ACK}`).count()) > 0;
|
|
513
|
+
} catch {
|
|
514
|
+
return false;
|
|
515
|
+
}
|
|
516
|
+
}, deps.stepTimeoutMs);
|
|
517
|
+
if (!ack) {
|
|
518
|
+
throw new Error(
|
|
519
|
+
`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`,
|
|
520
|
+
);
|
|
521
|
+
}
|
|
522
|
+
await uploadPodcastCover(deps, input.cover);
|
|
523
|
+
await podcastAdvanceReady(deps);
|
|
455
524
|
await leavePodcastModal(deps);
|
|
456
525
|
}
|
|
457
526
|
|
|
527
|
+
async function uploadPodcastCover(deps: ModeRunDeps, cover: string): Promise<void> {
|
|
528
|
+
const zone = deps.page.locator(PODCAST_COVER_ZONE).first();
|
|
529
|
+
const box = await zone.boundingBox().catch(() => null);
|
|
530
|
+
if (!box) {
|
|
531
|
+
throw new Error(`podcast modal: the cover card ('${PODCAST_COVER_ZONE}') has no bounding box to click`);
|
|
532
|
+
}
|
|
533
|
+
if (!deps.page.mouse) {
|
|
534
|
+
throw new Error('podcast modal: the page seam does not expose a raw mouse for the cover-card coordinate click');
|
|
535
|
+
}
|
|
536
|
+
const chooser = deps.page.waitForEvent?.('filechooser', { timeout: 8_000 }).catch(() => null) as Promise<
|
|
537
|
+
unknown | null
|
|
538
|
+
>;
|
|
539
|
+
await deps.page.mouse.click(box.x + box.width / 2, box.y + box.height / 2);
|
|
540
|
+
const pending = Symbol('pending');
|
|
541
|
+
const quick = await Promise.race([
|
|
542
|
+
chooser,
|
|
543
|
+
new Promise<typeof pending>((resolve) => setTimeout(() => resolve(pending), 400)),
|
|
544
|
+
]);
|
|
545
|
+
let diag = '';
|
|
546
|
+
if (quick === pending) {
|
|
547
|
+
const fallback = await deps.page
|
|
548
|
+
.evaluate(
|
|
549
|
+
`(() => {
|
|
550
|
+
const modal = document.querySelector('.d-modal') || document.body;
|
|
551
|
+
const inputs = Array.from(modal.querySelectorAll('input[type="file"]'));
|
|
552
|
+
const coverInput = inputs.find((i) => !String(i.getAttribute('accept') || '').includes('.mp3'));
|
|
553
|
+
const all = Array.from(modal.querySelectorAll('*')).filter((el) => (el.textContent || '').includes('设置封面'));
|
|
554
|
+
const leaf = all[all.length - 1];
|
|
555
|
+
let card = leaf;
|
|
556
|
+
while (card && card.parentElement && card.parentElement !== modal) {
|
|
557
|
+
const pt = card.parentElement.textContent || '';
|
|
558
|
+
if (pt.includes('上传成功') || pt.includes('点击上传') || pt.includes('去发布')) break;
|
|
559
|
+
card = card.parentElement;
|
|
560
|
+
}
|
|
561
|
+
let clickedInput = false;
|
|
562
|
+
if (coverInput && typeof coverInput.click === 'function') {
|
|
563
|
+
coverInput.click();
|
|
564
|
+
clickedInput = true;
|
|
565
|
+
} else {
|
|
566
|
+
if (card && typeof card.click === 'function') card.click();
|
|
567
|
+
if (leaf && typeof leaf.click === 'function') leaf.click();
|
|
568
|
+
}
|
|
569
|
+
const rect = card ? card.getBoundingClientRect() : null;
|
|
570
|
+
return JSON.stringify({
|
|
571
|
+
clickedInput,
|
|
572
|
+
x: rect ? Math.round(rect.left + rect.width / 2) : null,
|
|
573
|
+
y: rect ? Math.round(rect.top + rect.height * 0.35) : null,
|
|
574
|
+
inputs: inputs.map((i) => ({ accept: i.getAttribute('accept'), cls: i.className })),
|
|
575
|
+
cardTag: card ? card.tagName + '.' + String(card.className) : null,
|
|
576
|
+
cardRect: rect ? [Math.round(rect.left), Math.round(rect.top), Math.round(rect.width), Math.round(rect.height)] : null,
|
|
577
|
+
});
|
|
578
|
+
})()`,
|
|
579
|
+
)
|
|
580
|
+
.catch(() => '');
|
|
581
|
+
if (typeof fallback === 'string' && fallback.startsWith('{')) {
|
|
582
|
+
diag = ` [modal=${fallback}]`;
|
|
583
|
+
try {
|
|
584
|
+
const parsed = JSON.parse(fallback) as {
|
|
585
|
+
clickedInput?: boolean;
|
|
586
|
+
x?: number | null;
|
|
587
|
+
y?: number | null;
|
|
588
|
+
};
|
|
589
|
+
if (!parsed.clickedInput && typeof parsed.x === 'number' && typeof parsed.y === 'number') {
|
|
590
|
+
await deps.page.mouse.click(parsed.x, parsed.y);
|
|
591
|
+
}
|
|
592
|
+
} catch {
|
|
593
|
+
// ignore parse errors on diagnostic payload
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
const opened = quick === pending ? await chooser : quick;
|
|
598
|
+
if (!opened) {
|
|
599
|
+
throw new Error(
|
|
600
|
+
`podcast modal: clicking the cover card did not open a native file chooser within 8000ms${diag}`,
|
|
601
|
+
);
|
|
602
|
+
}
|
|
603
|
+
await (opened as { setFiles(path: string): Promise<void> }).setFiles(cover);
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
/** Wait until the advance control is enabled — `去发布` stays `disabled` until BOTH cards took. */
|
|
607
|
+
async function podcastAdvanceReady(deps: ModeRunDeps): Promise<void> {
|
|
608
|
+
if (deps.publishStatus !== 'draft') {
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
const enabled = await waitUntil(async () => {
|
|
612
|
+
try {
|
|
613
|
+
if ((await deps.page.locator('button:has-text("去发布")').count()) === 0) return false;
|
|
614
|
+
const disabled = await deps.page
|
|
615
|
+
.locator('button:has-text("去发布")[disabled], button:has-text("去发布")[aria-disabled="true"]')
|
|
616
|
+
.count();
|
|
617
|
+
return disabled === 0;
|
|
618
|
+
} catch {
|
|
619
|
+
return false;
|
|
620
|
+
}
|
|
621
|
+
}, deps.stepTimeoutMs);
|
|
622
|
+
if (!enabled) {
|
|
623
|
+
throw new Error(
|
|
624
|
+
`podcast modal: '${PODCAST_MODAL_ADVANCE[0]}' stayed disabled — the cards were not both accepted; check the audio ack '${PODCAST_UPLOAD_ACK}' and retry with the cover chooser path`,
|
|
625
|
+
);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
458
629
|
/** Compose the requested mode's post. Exhaustive over {@link ModeInput}. */
|
|
459
630
|
async function compose(input: ModeInput, deps: ModeRunDeps): Promise<void> {
|
|
460
631
|
switch (input.operation) {
|
|
@@ -58,6 +58,8 @@ export interface XhsLocator {
|
|
|
58
58
|
*/
|
|
59
59
|
type?(text: string, options?: { timeout?: number; delay?: number }): Promise<void>;
|
|
60
60
|
textContent?(): Promise<string>;
|
|
61
|
+
/** The card's click box — the native-chooser path clicks at its centre. */
|
|
62
|
+
boundingBox(): Promise<{ x: number; y: number; width: number; height: number } | null>;
|
|
61
63
|
}
|
|
62
64
|
|
|
63
65
|
/** Structural subset of the Playwright page the script driver uses. */
|
|
@@ -74,6 +76,10 @@ export interface XhsPage {
|
|
|
74
76
|
countOnScreen(selector: string): Promise<number>;
|
|
75
77
|
/** Click the first on-screen match — the console renders the tabs 3×. */
|
|
76
78
|
clickInViewport?(selector: string): Promise<boolean>;
|
|
79
|
+
/** The 发播客 cover card resolves its file through the site's native file chooser. */
|
|
80
|
+
waitForEvent?(event: string, options?: { timeout?: number }): Promise<unknown | null>;
|
|
81
|
+
/** Raw mouse press (the tab-bar clone debris needs coordinate clicks). */
|
|
82
|
+
mouse?: { click(x: number, y: number): Promise<void> };
|
|
77
83
|
/** Resolve a labelled control inside a closed shadow root. */
|
|
78
84
|
findShadowControl?(labels: readonly string[]): Promise<ShadowControl | null>;
|
|
79
85
|
/** Click at viewport coordinates. */
|
|
@@ -154,6 +160,7 @@ export function wrapPlaywrightPage(raw: unknown, shadow?: ShadowProbe): XhsPage
|
|
|
154
160
|
viewportSize?(): { width: number; height: number } | null;
|
|
155
161
|
evaluate(expression: string): Promise<unknown>;
|
|
156
162
|
mouse: { click(x: number, y: number): Promise<void> };
|
|
163
|
+
waitForEvent?: (event: string, options?: Record<string, unknown>) => Promise<unknown>;
|
|
157
164
|
};
|
|
158
165
|
|
|
159
166
|
/**
|
|
@@ -202,12 +209,22 @@ export function wrapPlaywrightPage(raw: unknown, shadow?: ShadowProbe): XhsPage
|
|
|
202
209
|
isVisible(o?: Record<string, unknown>): Promise<boolean>;
|
|
203
210
|
fill(t: string, o?: Record<string, unknown>): Promise<void>;
|
|
204
211
|
click(o?: Record<string, unknown>): Promise<void>;
|
|
212
|
+
boundingBox?(): Promise<{
|
|
213
|
+
x: number;
|
|
214
|
+
y: number;
|
|
215
|
+
width: number;
|
|
216
|
+
height: number;
|
|
217
|
+
} | null>;
|
|
205
218
|
};
|
|
206
219
|
return {
|
|
207
220
|
count: () => loc.count(),
|
|
208
221
|
isVisible: (options) => loc.isVisible(options),
|
|
209
222
|
fill: (text, options) => loc.fill(text, options),
|
|
210
223
|
click: (options) => loc.click(options),
|
|
224
|
+
// A `:has-text(...)` selector matches every ancestor `div` in pre-order before
|
|
225
|
+
// the leaf card itself; the last on-screen match is the innermost element.
|
|
226
|
+
boundingBox: async () =>
|
|
227
|
+
(await onScreen(selector)).at(-1)?.box ?? (await loc.boundingBox?.()) ?? null,
|
|
211
228
|
type: (text, options) =>
|
|
212
229
|
(
|
|
213
230
|
loc as unknown as {
|
|
@@ -232,6 +249,15 @@ export function wrapPlaywrightPage(raw: unknown, shadow?: ShadowProbe): XhsPage
|
|
|
232
249
|
},
|
|
233
250
|
findShadowControl: async (labels) => (shadow ? shadow.findControl(labels) : null),
|
|
234
251
|
clickAt: (x, y) => pw.mouse.click(x, y),
|
|
252
|
+
waitForEvent: (event, options) => {
|
|
253
|
+
if (typeof pw.waitForEvent !== 'function') {
|
|
254
|
+
return Promise.reject(new Error(`the page seam does not expose a '${event}' event source`));
|
|
255
|
+
}
|
|
256
|
+
return pw.waitForEvent(event, options) as Promise<unknown>;
|
|
257
|
+
},
|
|
258
|
+
mouse: {
|
|
259
|
+
click: (x, y) => pw.mouse.click(x, y),
|
|
260
|
+
},
|
|
235
261
|
};
|
|
236
262
|
}
|
|
237
263
|
|