@gobing-ai/knowledge-kit 0.0.33 → 0.0.35

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.
Files changed (69) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/dist/index.js +25191 -24167
  3. package/package.json +4 -4
  4. package/plugins/generations/content-gen/dist/index.js +9 -9
  5. package/plugins/generations/core-facts-gen/dist/index.js +12 -12
  6. package/plugins/generations/daily-article-gen/dist/index.js +31 -23
  7. package/plugins/generations/daily-article-gen/src/index.ts +39 -14
  8. package/plugins/generations/dailynews-gen/dist/index.js +23 -21
  9. package/plugins/generations/dailynews-gen/src/script-builder.ts +30 -26
  10. package/plugins/generations/episode-plan-gen/dist/index.js +12 -12
  11. package/plugins/generations/gemini-tts/dist/index.js +13 -12
  12. package/plugins/generations/image-gen/dist/index.js +26 -11
  13. package/plugins/generations/image-gen/presets/formats/xhs-card.yaml +16 -14
  14. package/plugins/generations/image-gen/presets/styles/xhs-knowledge.yaml +2 -2
  15. package/plugins/generations/image-gen/src/compose/card.ts +17 -2
  16. package/plugins/generations/news-report-gen/dist/index.js +12 -12
  17. package/plugins/generations/omni-voice-gen/plugin.json +1 -1
  18. package/plugins/generations/voice-gen/dist/index.js +13 -12
  19. package/plugins/generations/voice-gen/src/index.ts +9 -3
  20. package/plugins/ingestions/aihot-ingest/dist/index.js +12 -12
  21. package/plugins/ingestions/horizon-ingest/dist/index.js +12 -12
  22. package/plugins/ingestions/karakeep-local/dist/index.js +13 -13
  23. package/plugins/ingestions/last30days-ingest/dist/index.js +12 -12
  24. package/plugins/ingestions/web-search/dist/index.js +10 -10
  25. package/plugins/kk/decisions/extract/ai-news.yaml +6 -0
  26. package/plugins/kk/decisions/kk-items.yaml +69 -3
  27. package/plugins/kk/plugin.json +1 -1
  28. package/plugins/kk/scripts/daily-stages.ts +834 -87
  29. package/plugins/kk/scripts/daily-voice-style.zh.md +15 -10
  30. package/plugins/kk/scripts/kk-workflow-stages.ts +46 -3
  31. package/plugins/kk/skills/content-safety/SKILL.md +1 -1
  32. package/plugins/kk/skills/content-safety/references/platforms/apple-podcasts.md +2 -2
  33. package/plugins/kk/skills/content-safety/references/platforms/wechatmp.md +2 -2
  34. package/plugins/kk/skills/content-safety/references/platforms/xiaoyuzhou.md +4 -2
  35. package/plugins/kk/workflows/kk-collect.yaml +7 -9
  36. package/plugins/kk/workflows/kk-daily-ai-voice.yaml +32 -7
  37. package/plugins/kk/workflows/kk-daily-ai-voice2.yaml +169 -47
  38. package/plugins/kk/workflows/kk-daily-safety.yaml +9 -12
  39. package/plugins/publishings/emdash-pub/dist/index.js +9 -9
  40. package/plugins/publishings/infoq-pub/dist/index.js +22 -22
  41. package/plugins/publishings/infoq-pub/dist/profile-check.js +30336 -0
  42. package/plugins/publishings/infoq-pub/plugin.json +1 -1
  43. package/plugins/publishings/juejin-pub/dist/index.js +22 -22
  44. package/plugins/publishings/juejin-pub/dist/profile-check.js +30322 -0
  45. package/plugins/publishings/juejin-pub/plugin.json +1 -1
  46. package/plugins/publishings/medium-pub/dist/index.js +9 -9
  47. package/plugins/publishings/podcast-pub/dist/index.js +9 -9
  48. package/plugins/publishings/qiita-pub/dist/index.js +9 -9
  49. package/plugins/publishings/substack-pub/dist/index.js +22 -22
  50. package/plugins/publishings/substack-pub/dist/profile-check.js +30392 -0
  51. package/plugins/publishings/substack-pub/plugin.json +1 -1
  52. package/plugins/publishings/surfdash-pub/dist/index.js +9 -9
  53. package/plugins/publishings/wechatmp-pub/dist/index.js +329 -154
  54. package/plugins/publishings/wechatmp-pub/dist/profile-check.js +30271 -0
  55. package/plugins/publishings/wechatmp-pub/plugin.json +1 -1
  56. package/plugins/publishings/wechatmp-pub/src/enrich.ts +106 -145
  57. package/plugins/publishings/wechatmp-pub/src/flow.ts +283 -42
  58. package/plugins/publishings/wechatmp-pub/src/page.ts +20 -0
  59. package/plugins/publishings/x-pub/dist/index.js +22 -22
  60. package/plugins/publishings/x-pub/dist/profile-check.js +30328 -0
  61. package/plugins/publishings/x-pub/plugin.json +1 -1
  62. package/plugins/publishings/xhs-pub/dist/index.js +76 -27
  63. package/plugins/publishings/xhs-pub/dist/profile-check.js +30538 -0
  64. package/plugins/publishings/xhs-pub/plugin.json +1 -1
  65. package/plugins/publishings/xhs-pub/src/flow.ts +9 -3
  66. package/plugins/publishings/xhs-pub/src/index.ts +111 -5
  67. package/plugins/publishings/zenn-pub/dist/index.js +9 -9
  68. package/plugins/generations/omni-voice-gen/dist/omni-voice-gen-prr8skpb. +0 -2
  69. package/plugins/generations/omni-voice-gen/dist/omni-voice-gen.js +0 -6
@@ -3,7 +3,7 @@ import { ResultSchema } from '@gobing-ai/kk-core';
3
3
  import { trySelectors } from '@gobing-ai/publish-harness';
4
4
  import { expectOutcome, pasteHtml, readScopedSignal, uploadFile } from '@gobing-ai/publish-harness/browser';
5
5
  import type { Page } from 'playwright';
6
- import { enrichArticle, insertAudio } from './enrich';
6
+ import { enrichArticle } from './enrich';
7
7
  import {
8
8
  artifactPaths,
9
9
  CREATE_TYPES,
@@ -23,6 +23,7 @@ import {
23
23
  followNewPage,
24
24
  tryFindSelector,
25
25
  type WechatPage,
26
+ waitForVisible,
26
27
  } from './page';
27
28
 
28
29
  /**
@@ -65,21 +66,33 @@ const LAYOUT_TARGET = ['div[role="menuitem"]:has-text("匹配目标格式")', 'l
65
66
 
66
67
  /**
67
68
  * 封面 has NO file input (§5.2): the panel offers `修改` / `换一张` / `从正文选择`
68
- * (`a.js_selectCoverFromContent`) / 从图片库选择 / 微信扫码上传 / AI 配图, and states 默认首图为封面.
69
- * Only the body-image path is implemented; the other three are recorded non-goals (0205).
69
+ * (`a.js_selectCoverFromContent`) / `从图片库选择` (`a.js_imagedialog`) / 微信扫码上传 / AI 配图.
70
+ * `从图片库选择` is the default because the daily flow uploads the episode cover there as the audio
71
+ * material's cover, so the library holds exactly the image the operator wants (2026-10-10);
72
+ * `从正文选择` remains selectable via `metadata.wechatmp.coverSource`.
70
73
  */
71
74
  const COVER_PANEL_OPEN = ['a.js_selectCoverFromContent', '.js_cover_btn_area', 'text=换一张', 'text=修改'];
72
75
  const COVER_FROM_CONTENT = ['a.js_selectCoverFromContent', 'text=从正文选择'];
76
+ const COVER_FROM_LIBRARY = ['a.js_imagedialog', 'text=从图片库选择'];
77
+ /**
78
+ * The library picker's item list — the same `not <img>-based` widget as the body-source panel, so
79
+ * these stay candidate shapes. The newest upload sorts first (operator walk 2026-10-10: the just-uploaded
80
+ * audio cover is the default selection, `默认第一个`), so the first item is the intended one.
81
+ */
82
+ const COVER_LIBRARY_ITEM = [
83
+ 'div.weui-desktop-img-picker__item',
84
+ 'div[class*="img_picker" i] li',
85
+ 'div[class*="imgList" i] li',
86
+ 'div[class*="cover" i] li',
87
+ ];
88
+ /** The library picker is two-stage: pick the image, then 下一步, then the dialog's 确认. */
89
+ const COVER_LIBRARY_NEXT = ['button:has-text("下一步")'];
73
90
  /**
74
91
  * The panel's own image list is NOT `<img>`-based (measured `panels: 5, imgs: 0`), so the item and
75
92
  * confirm selectors below are candidate shapes and **not** re-measured (§6 still lists the widget as
76
93
  * to do). A miss is recorded as a warning, never as a false cover claim.
77
94
  */
78
- const COVER_ITEM = [
79
- 'div[class*="cover" i] li',
80
- 'div[class*="imgList" i] li',
81
- 'div[class*="weui-desktop-img-picker" i] li',
82
- ];
95
+ const COVER_ITEM = COVER_LIBRARY_ITEM;
83
96
  const COVER_CONFIRM = ['button:has-text("确定")', 'button:has-text("确认")', 'button:has-text("完成")'];
84
97
 
85
98
  /** 视频's media modal (§5.2): tabs 素材库/视频号动态/视频链接 + 本地上传 + 确定/取消. */
@@ -111,6 +124,37 @@ const PODCAST_COVER_INPUT = [
111
124
  ];
112
125
  const PODCAST_INSERT = ['div.weui-desktop-dialog button:has-text("插入")', 'button:has-text("插入")'];
113
126
 
127
+ /** 音频 material dialog: the toolbar menu opens it, then the 素材库 list or the upload form. */
128
+ const AUDIO_MENU = 'li.js_insertaudio';
129
+ const AUDIO_DIALOG = '.audio_dialog';
130
+ /** The 素材库 rows stream in after the dialog opens, so the dedupe probe retries while they load. */
131
+ const AUDIO_DIALOG_TIMEOUT_MS = 15_000;
132
+ const AUDIO_ROW_RETRIES = 2;
133
+ const AUDIO_ROW_SETTLE_MS = 1_200;
134
+ /**
135
+ * The upload form (operator walk 2026-10-10: `上传音频` → file → `上传封面` → file → title → agree →
136
+ * 保存). The form lives inside the audio dialog, so the file inputs stay scoped to it.
137
+ */
138
+ const UPLOAD_AUDIO = ['text=上传音频', 'button:has-text("上传音频")'];
139
+ const AUDIO_UPLOAD_INPUT = [
140
+ '.weui-desktop-dialog input[type="file"][accept*="audio"]',
141
+ '.weui-desktop-dialog input[type="file"][accept*=".mp3"]',
142
+ '.audio_dialog input[type="file"][accept*="audio"]',
143
+ 'input[type="file"][accept*=".mp3"]',
144
+ ];
145
+ const UPLOAD_COVER = ['text=上传封面', 'button:has-text("上传封面")'];
146
+ const AUDIO_COVER_INPUT = [
147
+ '.weui-desktop-dialog input[type="file"][accept*="image/"]',
148
+ '.audio_dialog input[type="file"][accept*="image/"]',
149
+ 'input[type="file"][accept*=".png"]',
150
+ ];
151
+ const AUDIO_TITLE_INPUT = [
152
+ '.weui-desktop-dialog input[placeholder*="标题"]',
153
+ '.audio_dialog input[placeholder*="标题"]',
154
+ '.audio_dialog input.frm_input',
155
+ ];
156
+ const UPLOAD_SAVE = ['.weui-desktop-dialog button:has-text("保存")', 'button:has-text("保存")'];
157
+
114
158
  /** The recorded save control. `发表` is never clicked on a draft run (ADR-022/ADR-026). */
115
159
  const SAVE_DRAFT = ['button:has-text("保存为草稿")', 'text=保存为草稿'];
116
160
  const PUBLISH_SEND = ['button.mass_send', '.mass_send', 'button:has-text("发表")'];
@@ -452,20 +496,35 @@ async function insertBody(page: WechatPage, deps: ModeRunDeps): Promise<void> {
452
496
  }
453
497
 
454
498
  /**
455
- * `一键排版` applies the layout directly. Measured live (dogfood 2026-10-05): after the click the only
456
- * new text was ONE element carrying `保留源格式 匹配目标格式 仅保留文本` as a label — a format hint, not a
457
- * clickable option set (the discovery note misread that concatenation as a menu). Waiting 30 s for
458
- * `匹配目标格式` therefore failed a real draft. Click the control when present, honour an option only
459
- * if one appears promptly, and never let the layout step gate the save.
499
+ * `一键排版` applies the platform's own layout to the pasted body. The operator walk of 2026-10-10 shows
500
+ * the control is present and expected to be applied, so a click that leads nowhere is now REPORTED
501
+ * rather than swallowed (the earlier version failed silently, which is why 正文 came out unformatted).
502
+ *
503
+ * The format set (`保留源格式` / `匹配目标格式` / `仅保留文本`) is what we want when it renders as a menu;
504
+ * `匹配目标格式` is the correct choice for markdown-derived content. A build where the trio is only a
505
+ * label (the 2026-10-05 dogfood reading) still gets the bare control click, and says so.
460
506
  */
461
- async function applyLayout(page: WechatPage, deps: ModeRunDeps): Promise<void> {
507
+ async function applyLayout(page: WechatPage, deps: ModeRunDeps): Promise<string | null> {
508
+ let control: string;
509
+ try {
510
+ control = await findSelector(page, LAYOUT_CONTROL, '一键排版 control', deps.stepTimeoutMs);
511
+ } catch (error) {
512
+ return `一键排版: ${error instanceof Error ? error.message : String(error)}`;
513
+ }
462
514
  try {
463
- const control = await findSelector(page, LAYOUT_CONTROL, '一键排版 control', deps.stepTimeoutMs);
464
515
  await clickSelector(page, control, deps.stepTimeoutMs);
465
- const option = await findSelector(page, LAYOUT_TARGET, '匹配目标格式 option', 1500).catch(() => null);
466
- if (option !== null) await clickSelector(page, option, 1500);
467
- } catch {
468
- // No layout control on this editor build — the draft still saves.
516
+ } catch (error) {
517
+ return `一键排版: click failed — ${error instanceof Error ? error.message : String(error)}`;
518
+ }
519
+ const option = await tryFindSelector(page, LAYOUT_TARGET, 5_000);
520
+ if (option === undefined) {
521
+ return '一键排版: clicked, but no 匹配目标格式 option appeared — the body may be unformatted; check the draft';
522
+ }
523
+ try {
524
+ await clickSelector(page, option, 5_000);
525
+ return null;
526
+ } catch (error) {
527
+ return `一键排版: 匹配目标格式 not applied — ${error instanceof Error ? error.message : String(error)}`;
469
528
  }
470
529
  }
471
530
 
@@ -490,24 +549,29 @@ async function fillOptional(
490
549
  }
491
550
 
492
551
  /**
493
- * 封面 from the body (§5.2): there is no cover file input, so the panel's `从正文选择` picks the first
494
- * body image. Best-effort and recorded: the panel states 默认首图为封面, so the first body image is
495
- * already the cover, and only the explicit pick's failure is reported as a warning.
552
+ * 封面 (§5.2): there is no cover file input. `从图片库选择` picks the episode cover, which the daily
553
+ * flow has already uploaded as the audio material's cover; `从正文选择` picks the first body image,
554
+ * which is what the platform's 默认首图为封面 fallback would use anyway. Best-effort and recorded: a
555
+ * miss is a warning naming the step, never a false claim that the cover landed.
496
556
  */
497
- async function setCoverFromBody(page: WechatPage, deps: ModeRunDeps): Promise<string | null> {
498
- const open = await tryFindSelector(page, COVER_PANEL_OPEN, Math.min(deps.stepTimeoutMs, 5_000));
499
- if (open === undefined)
500
- return 'cover: the panel offers neither 从正文选择 nor its openers — the platform default (默认首图为封面) is all this run has';
557
+ async function setCover(page: WechatPage, source: 'library' | 'body', deps: ModeRunDeps): Promise<string | null> {
558
+ const picker = source === 'library' ? COVER_FROM_LIBRARY : COVER_FROM_CONTENT;
559
+ const what = source === 'library' ? '从图片库选择' : '从正文选择';
501
560
  try {
561
+ const open = await tryFindSelector(page, COVER_PANEL_OPEN, Math.min(deps.stepTimeoutMs, 5_000));
562
+ if (open === undefined)
563
+ return 'cover: the panel offers none of 从正文选择 / 从图片库选择 — the platform default (默认首图为封面) is all this run has';
502
564
  await clickSelector(page, open, deps.stepTimeoutMs);
503
- const pick = await tryFindSelector(page, COVER_FROM_CONTENT, Math.min(deps.stepTimeoutMs, 5_000));
504
- if (pick === undefined) return 'cover: 从正文选择 was not offered by the open cover panel';
565
+ const pick = await tryFindSelector(page, picker, Math.min(deps.stepTimeoutMs, 8_000));
566
+ if (pick === undefined) return `cover: ${what} was not offered by the open cover panel`;
505
567
  await clickSelector(page, pick, deps.stepTimeoutMs);
506
- const item = await tryFindSelector(page, COVER_ITEM, Math.min(deps.stepTimeoutMs, 5_000));
507
- if (item === undefined)
508
- return 'cover: the panel listed no body image to choose (its list is not <img>-based, §5.2)';
568
+ const item = await tryFindSelector(page, COVER_ITEM, Math.min(deps.stepTimeoutMs, 8_000));
569
+ if (item === undefined) return `cover: ${what} listed no image to choose (its list is not <img>-based, §5.2)`;
509
570
  await clickSelector(page, item, deps.stepTimeoutMs);
510
- const confirm = await tryFindSelector(page, COVER_CONFIRM, Math.min(deps.stepTimeoutMs, 5_000));
571
+ // The library picker stages the choice: 下一步 advances it, then the dialog confirms.
572
+ const next = await tryFindSelector(page, COVER_LIBRARY_NEXT, Math.min(deps.stepTimeoutMs, 4_000));
573
+ if (next !== undefined) await clickSelector(page, next, deps.stepTimeoutMs);
574
+ const confirm = await tryFindSelector(page, COVER_CONFIRM, Math.min(deps.stepTimeoutMs, 8_000));
511
575
  if (confirm === undefined) return 'cover: the choice panel offered no confirm control';
512
576
  await clickSelector(page, confirm, deps.stepTimeoutMs);
513
577
  return null;
@@ -516,20 +580,39 @@ async function setCoverFromBody(page: WechatPage, deps: ModeRunDeps): Promise<st
516
580
  }
517
581
  }
518
582
 
519
- /** 文章's `metadata.wechatmp` enrichment rail, plus its opt-in audio embed (§5.2, ADR-026). */
583
+ /** `metadata.wechatmp.coverSource` — `library` (default) is the uploaded episode cover. */
584
+ function coverSourceOf(content: Content): 'library' | 'body' {
585
+ const meta = content.metadata?.wechatmp;
586
+ if (meta === null || typeof meta !== 'object' || Array.isArray(meta)) return 'library';
587
+ return (meta as Record<string, unknown>).coverSource === 'body' ? 'body' : 'library';
588
+ }
589
+
590
+ /** 文章's `metadata.wechatmp` enrichment rail, plus its opt-in audio embed (2026-09-29, ADR-026). */
520
591
  async function enrichArticleDraft(page: WechatPage, content: Content): Promise<string[]> {
521
592
  const enrichMeta = content.metadata?.wechatmp;
522
593
  if (enrichMeta === undefined || typeof enrichMeta !== 'object' || Array.isArray(enrichMeta)) return [];
523
- const warnings = await enrichArticle(page, enrichMeta as Record<string, unknown>);
524
- const audioTitle = (enrichMeta as Record<string, unknown>).audioTitle;
525
- if (typeof audioTitle === 'string' && audioTitle.trim() !== '') {
526
- const audioWarning = await insertAudio(page, audioTitle.trim());
594
+ const meta = enrichMeta as Record<string, unknown>;
595
+ const warnings = await enrichArticle(page, meta);
596
+ const audioTitle = meta.audioTitle;
597
+ const audioPath = meta.audioPath;
598
+ const audioCoverPath = meta.audioCoverPath;
599
+ if (
600
+ typeof audioTitle === 'string' &&
601
+ audioTitle.trim() !== '' &&
602
+ typeof audioPath === 'string' &&
603
+ audioPath !== ''
604
+ ) {
605
+ const audioWarning = await insertAudioMaterial(page, {
606
+ title: audioTitle.trim(),
607
+ audioPath,
608
+ coverPath: typeof audioCoverPath === 'string' ? audioCoverPath : '',
609
+ });
527
610
  if (audioWarning !== '') warnings.push(audioWarning);
528
611
  }
529
612
  return warnings;
530
613
  }
531
614
 
532
- /** Compose 文章 (createType 0): body images, title, body + 一键排版, 摘要, 作者, cover from the body. */
615
+ /** Compose 文章 (createType 0): body images, title, body + 一键排版, 摘要, 作者, cover, settings. */
533
616
  async function composeArticle(
534
617
  input: { operation: '文章'; images: string[]; digest?: string },
535
618
  deps: ModeRunDeps,
@@ -540,14 +623,15 @@ async function composeArticle(
540
623
  await attachImages(page, input.images, deps);
541
624
  const titlePath = await typeTitle(page, deps);
542
625
  await insertBody(page, deps);
543
- await applyLayout(page, deps);
626
+ const layout = await applyLayout(page, deps);
627
+ if (layout !== null) warnings.push(layout);
544
628
  for (const warning of [
545
629
  await fillOptional(page, DIGEST_INPUT, input.digest ?? '', '摘要', deps),
546
630
  await fillOptional(page, AUTHOR_INPUT, authorOf(deps.content), '作者', deps),
547
631
  ]) {
548
632
  if (warning !== null) warnings.push(warning);
549
633
  }
550
- const cover = await setCoverFromBody(page, deps);
634
+ const cover = await setCover(page, coverSourceOf(deps.content), deps);
551
635
  if (cover !== null) warnings.push(cover);
552
636
  return { warnings, titlePath, enrichWarnings: await enrichArticleDraft(page, deps.content) };
553
637
  }
@@ -647,8 +731,18 @@ async function compose(input: ModeInput, deps: ModeRunDeps, entry: ModeEntry): P
647
731
  }
648
732
 
649
733
  function authorOf(content: Content): string {
650
- const author = content.metadata?.author;
651
- return typeof author === 'string' ? author : '';
734
+ // The 作者 field is the byline readers see. The daily stage carries it as `metadata.wechatmp.author`
735
+ // (with the other MP preferences); `metadata.author` is the generic Content field and wins when a
736
+ // producer sets it directly. Reading only the generic one left 请输入作者 empty on every daily run
737
+ // (operator report 2026-10-10).
738
+ const wechatmp = content.metadata?.wechatmp;
739
+ const scoped =
740
+ wechatmp !== null && typeof wechatmp === 'object' && !Array.isArray(wechatmp)
741
+ ? (wechatmp as Record<string, unknown>).author
742
+ : undefined;
743
+ const generic = content.metadata?.author;
744
+ if (typeof generic === 'string' && generic.trim() !== '') return generic.trim();
745
+ return typeof scoped === 'string' ? scoped.trim() : '';
652
746
  }
653
747
 
654
748
  function baseMetadata(input: ModeInput, deps: ModeRunDeps, entry: ModeEntry): Record<string, unknown> {
@@ -894,3 +988,150 @@ export function markdownToWechatHtml(markdown: string): string {
894
988
  function escapeHtml(text: string): string {
895
989
  return text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
896
990
  }
991
+
992
+ /**
993
+ * Audio at the article top (operator request 2026-09-29; upload path added 2026-10-10).
994
+ *
995
+ * Reuses a same-title 素材库 material when one exists; otherwise UPLOADS it. The operator's measured
996
+ * upload shape: 音频 menu → 上传音频 → pick the audio file → 上传封面 → pick the cover → type the title
997
+ * → tick 我已阅读并同意《公众平台音频上传服务规则》 → 保存 → the material list reopens → select the
998
+ * default (first) row → 插入. 插入 places it at the caret, so the caret is put at the body start first.
999
+ *
1000
+ * Fail-soft — every non-committed outcome returns a warning string, `''` means placed at the top.
1001
+ */
1002
+ export async function insertAudioMaterial(
1003
+ page: WechatPage,
1004
+ input: { title: string; audioPath: string; coverPath: string },
1005
+ ): Promise<string> {
1006
+ const { title, audioPath, coverPath } = input;
1007
+ const CARET = `(() => {
1008
+ const pm = [...document.querySelectorAll('.ProseMirror')].find((e) => !(e.getAttribute('data-placeholder') ?? '').includes('标题'));
1009
+ if (!pm || !pm.firstChild) return;
1010
+ const r = document.createRange();
1011
+ r.setStart(pm.firstChild, 0);
1012
+ r.collapse(true);
1013
+ const sel = window.getSelection();
1014
+ sel?.removeAllRanges();
1015
+ sel?.addRange(r);
1016
+ })()`;
1017
+ const DEDUPE = `(() => {
1018
+ const want = ${JSON.stringify(title)}.replace(/\\s+/g, '');
1019
+ const hit = [...document.querySelectorAll('label.frm_checkbox_label.audio_item')]
1020
+ .find((l) => (l.textContent ?? '').replace(/\\s+/g, '').includes(want));
1021
+ if (!hit) return 'missing';
1022
+ const cb = (hit.closest('li') ?? hit.parentElement)?.querySelector('input[type=checkbox]');
1023
+ if (!cb) return 'nobox';
1024
+ cb.click();
1025
+ return 'checked';
1026
+ })()`;
1027
+ /** Tick the service-rules acknowledgement, found by its own label text rather than a class. */
1028
+ const AGREE = `(() => {
1029
+ const node = [...document.querySelectorAll('label, span, div')]
1030
+ .find((e) => (e.textContent ?? '').includes('我已阅读并同意') && e.children.length === 0);
1031
+ if (!node) return 'no-label';
1032
+ const scope = node.closest('label') ?? node.parentElement;
1033
+ const cb = scope?.querySelector('input[type=checkbox]') ?? document.querySelector('input[type=checkbox].js_agree');
1034
+ if (!cb) return 'no-box';
1035
+ if (!cb.checked) cb.click();
1036
+ return cb.checked ? 'checked' : 'unchecked';
1037
+ })()`;
1038
+ /** Select the material the upload just created — the operator's 默认第一个. */
1039
+ const SELECT_FIRST = `(() => {
1040
+ const row = document.querySelector('label.frm_checkbox_label.audio_item');
1041
+ if (!row) return 'norow';
1042
+ const cb = (row.closest('li') ?? row.parentElement)?.querySelector('input[type=checkbox]');
1043
+ if (!cb) return 'nobox';
1044
+ if (!cb.checked) cb.click();
1045
+ return 'checked';
1046
+ })()`;
1047
+ const INSERT = `(() => {
1048
+ const btn = [...document.querySelectorAll('button')].find((b) => (b.textContent ?? '').trim() === '插入' && b.offsetWidth > 0);
1049
+ if (!btn) return false;
1050
+ btn.click();
1051
+ return true;
1052
+ })()`;
1053
+ const CANCEL = `(() => {
1054
+ const btn = [...document.querySelectorAll('button')].find((b) => (b.textContent ?? '').trim() === '取消' && b.offsetWidth > 0);
1055
+ btn?.click();
1056
+ })()`;
1057
+ const VERIFY = `(() => {
1058
+ const pm = [...document.querySelectorAll('.ProseMirror')].find((e) => !(e.getAttribute('data-placeholder') ?? '').includes('标题'));
1059
+ if (!pm) return 'noeditor';
1060
+ const node = pm.querySelector('mpvoice, [class*="audio"]');
1061
+ if (!node) return 'absent';
1062
+ const first = pm.firstElementChild;
1063
+ return first && (first === node || first.contains(node)) ? 'top' : 'notop';
1064
+ })()`;
1065
+
1066
+ /** Upload the audio + its cover, acknowledge the rules, and 保存. Returns a warning or null. */
1067
+ const upload = async (): Promise<string | null> => {
1068
+ const uploadControl = await trySelectors(page, UPLOAD_AUDIO);
1069
+ if (!uploadControl.found) return '音频: 上传音频 control not found in the audio dialog';
1070
+ await clickSelector(page, UPLOAD_AUDIO.join(', '), 8_000);
1071
+ const audioInput = await trySelectors(page, AUDIO_UPLOAD_INPUT);
1072
+ if (!audioInput.found) return '音频: no audio file input appeared';
1073
+ await uploadFile(asPlaywrightPage(page), audioInput.selector ?? AUDIO_UPLOAD_INPUT.join(', '), audioPath, {
1074
+ timeoutMs: 60_000,
1075
+ });
1076
+ // The form requires a cover before it will save.
1077
+ const coverControl = await trySelectors(page, UPLOAD_COVER);
1078
+ if (coverControl.found) {
1079
+ await clickSelector(page, UPLOAD_COVER.join(', '), 8_000);
1080
+ const coverInput = await trySelectors(page, AUDIO_COVER_INPUT);
1081
+ if (coverInput.found)
1082
+ await uploadFile(
1083
+ asPlaywrightPage(page),
1084
+ coverInput.selector ?? AUDIO_COVER_INPUT.join(', '),
1085
+ coverPath,
1086
+ {
1087
+ timeoutMs: 60_000,
1088
+ },
1089
+ );
1090
+ }
1091
+ const titleInput = await trySelectors(page, AUDIO_TITLE_INPUT);
1092
+ if (titleInput.found) await fillSelector(page, titleInput.selector ?? AUDIO_TITLE_INPUT.join(', '), title);
1093
+ const agreed = String(await page.evaluate(AGREE));
1094
+ if (agreed !== 'checked') return `音频: 服务规则勾选失败 (${agreed})`;
1095
+ const save = await trySelectors(page, UPLOAD_SAVE);
1096
+ if (!save.found) return '音频: 上传表单的保存按钮未找到';
1097
+ await clickSelector(page, UPLOAD_SAVE.join(', '), 8_000);
1098
+ return null;
1099
+ };
1100
+
1101
+ try {
1102
+ await page.evaluate(CARET);
1103
+ await page.evaluate(`document.querySelector(${JSON.stringify(AUDIO_MENU)})?.click()`);
1104
+ await waitForVisible(page, AUDIO_DIALOG, AUDIO_DIALOG_TIMEOUT_MS);
1105
+ // The 素材库 list loads async after the dialog opens — retry while rows stream in.
1106
+ let dedupe = String(await page.evaluate(DEDUPE));
1107
+ for (let i = 0; dedupe === 'missing' && i < AUDIO_ROW_RETRIES; i += 1) {
1108
+ await page.waitForTimeout?.(AUDIO_ROW_SETTLE_MS);
1109
+ dedupe = String(await page.evaluate(DEDUPE));
1110
+ }
1111
+ if (dedupe !== 'checked') {
1112
+ // No reusable material: upload one, which reopens the list with it selected-by-default.
1113
+ const uploadWarning = await upload();
1114
+ if (uploadWarning !== null) {
1115
+ await page.evaluate(CANCEL);
1116
+ return uploadWarning;
1117
+ }
1118
+ await waitForVisible(page, AUDIO_DIALOG, AUDIO_DIALOG_TIMEOUT_MS);
1119
+ const selected = String(await page.evaluate(SELECT_FIRST));
1120
+ if (selected !== 'checked') {
1121
+ await page.evaluate(CANCEL);
1122
+ return `音频: 上传后未找到素材行 (${selected})`;
1123
+ }
1124
+ }
1125
+ if ((await page.evaluate(INSERT)) !== true) {
1126
+ await page.evaluate(CANCEL);
1127
+ return '音频: 插入按钮未找到,请手动插入';
1128
+ }
1129
+ await page.waitForTimeout?.(2_000);
1130
+ const state = String(await page.evaluate(VERIFY));
1131
+ if (state === 'absent' || state === 'noeditor') return '音频: 插入未生效,请手动检查';
1132
+ if (state === 'notop') return '音频: 已插入但未在文首,请手动调整位置';
1133
+ return '';
1134
+ } catch (error) {
1135
+ return `音频: ${error instanceof Error ? error.message.slice(0, 120) : '自动插入失败'},请手动插入`;
1136
+ }
1137
+ }
@@ -258,3 +258,23 @@ export function wrapPlaywrightContext(raw: unknown, entryPage: unknown): OpenedC
258
258
  close: () => context.close(),
259
259
  };
260
260
  }
261
+
262
+ /** Polls a selector list for visibility; optionally returns `undefined` instead of throwing. */
263
+ export async function waitForVisible(
264
+ page: WechatPage,
265
+ selector: string,
266
+ timeoutMs: number,
267
+ opts: { required?: boolean } = {},
268
+ ): Promise<string | undefined> {
269
+ const visible = await waitUntil(
270
+ async () => {
271
+ const found = await trySelectors(page, [selector]).catch(() => ({ found: false, selector: null }));
272
+ return found.found;
273
+ },
274
+ timeoutMs,
275
+ POLL_MS,
276
+ );
277
+ if (visible) return selector;
278
+ if (opts.required === false) return undefined;
279
+ throw new Error(`${selector}: not visible within ${timeoutMs}ms`);
280
+ }
@@ -14671,7 +14671,7 @@ var init_zod = __esm(() => {
14671
14671
  init_external();
14672
14672
  });
14673
14673
 
14674
- // ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.18/node_modules/@gobing-ai/ts-utils/dist/errors.js
14674
+ // ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.21/node_modules/@gobing-ai/ts-utils/dist/errors.js
14675
14675
  var ErrorCode;
14676
14676
  var init_errors3 = __esm(() => {
14677
14677
  ErrorCode = {
@@ -14682,7 +14682,7 @@ var init_errors3 = __esm(() => {
14682
14682
  };
14683
14683
  });
14684
14684
 
14685
- // ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.18/node_modules/@gobing-ai/ts-utils/dist/api-response.js
14685
+ // ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.21/node_modules/@gobing-ai/ts-utils/dist/api-response.js
14686
14686
  var API_ERROR_CODES, ERROR_CODE_TO_HTTP, CLIENT_SAFE_CODES;
14687
14687
  var init_api_response = __esm(() => {
14688
14688
  init_errors3();
@@ -14705,10 +14705,10 @@ var init_api_response = __esm(() => {
14705
14705
  CLIENT_SAFE_CODES = new Set([ErrorCode.NotFound, ErrorCode.Validation, ErrorCode.Conflict]);
14706
14706
  });
14707
14707
 
14708
- // ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.18/node_modules/@gobing-ai/ts-utils/dist/cursor.js
14708
+ // ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.21/node_modules/@gobing-ai/ts-utils/dist/cursor.js
14709
14709
  var init_cursor = () => {};
14710
14710
 
14711
- // ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.18/node_modules/@gobing-ai/ts-utils/dist/env.js
14711
+ // ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.21/node_modules/@gobing-ai/ts-utils/dist/env.js
14712
14712
  function getEnvVar(name, fallback) {
14713
14713
  const raw = process.env[name];
14714
14714
  return raw === undefined ? fallback : raw;
@@ -14717,10 +14717,10 @@ function getEnvVars() {
14717
14717
  return process.env;
14718
14718
  }
14719
14719
 
14720
- // ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.18/node_modules/@gobing-ai/ts-utils/dist/event-severity.js
14720
+ // ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.21/node_modules/@gobing-ai/ts-utils/dist/event-severity.js
14721
14721
  var init_event_severity = () => {};
14722
14722
 
14723
- // ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.18/node_modules/@gobing-ai/ts-utils/dist/object.js
14723
+ // ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.21/node_modules/@gobing-ai/ts-utils/dist/object.js
14724
14724
  function isPlainObject2(value) {
14725
14725
  return typeof value === "object" && value !== null && !Array.isArray(value);
14726
14726
  }
@@ -14738,7 +14738,7 @@ function deepMerge(target, source) {
14738
14738
  return result;
14739
14739
  }
14740
14740
 
14741
- // ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.18/node_modules/@gobing-ai/ts-utils/dist/output.js
14741
+ // ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.21/node_modules/@gobing-ai/ts-utils/dist/output.js
14742
14742
  function processStream(name) {
14743
14743
  const proc = globalThis.process;
14744
14744
  const stream = proc?.[name];
@@ -14756,7 +14756,7 @@ function echoError(message, target = defaultStderrTarget ?? processStream("stder
14756
14756
  }
14757
14757
  var defaultStderrTarget;
14758
14758
 
14759
- // ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.18/node_modules/@gobing-ai/ts-utils/dist/index.js
14759
+ // ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.21/node_modules/@gobing-ai/ts-utils/dist/index.js
14760
14760
  var init_dist = __esm(() => {
14761
14761
  init_api_response();
14762
14762
  init_cursor();
@@ -21754,7 +21754,7 @@ var init_dist2 = __esm(() => {
21754
21754
  $visitAsync = visit.visitAsync;
21755
21755
  });
21756
21756
 
21757
- // ../../node_modules/.bun/@gobing-ai+ts-runtime@0.5.18+ea28d47622d670ff/node_modules/@gobing-ai/ts-runtime/dist/config.js
21757
+ // ../../node_modules/.bun/@gobing-ai+ts-runtime@0.5.21+44edd5c4d6372504/node_modules/@gobing-ai/ts-runtime/dist/config.js
21758
21758
  function getProcessEnv() {
21759
21759
  return getEnvVars();
21760
21760
  }
@@ -21832,7 +21832,7 @@ var init_config = __esm(() => {
21832
21832
  };
21833
21833
  });
21834
21834
 
21835
- // ../../node_modules/.bun/@gobing-ai+ts-runtime@0.5.18+ea28d47622d670ff/node_modules/@gobing-ai/ts-runtime/dist/file-system-node.js
21835
+ // ../../node_modules/.bun/@gobing-ai+ts-runtime@0.5.21+44edd5c4d6372504/node_modules/@gobing-ai/ts-runtime/dist/file-system-node.js
21836
21836
  import { appendFileSync, cpSync, createReadStream, createWriteStream, existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from "fs";
21837
21837
  import { dirname, resolve as resolvePath } from "path";
21838
21838
  function createNodeFileSystem(root, paths) {
@@ -21919,7 +21919,7 @@ function findProjectRoot(startDir) {
21919
21919
  }
21920
21920
  var init_file_system_node = () => {};
21921
21921
 
21922
- // ../../node_modules/.bun/@gobing-ai+ts-runtime@0.5.18+ea28d47622d670ff/node_modules/@gobing-ai/ts-runtime/dist/db-errors.js
21922
+ // ../../node_modules/.bun/@gobing-ai+ts-runtime@0.5.21+44edd5c4d6372504/node_modules/@gobing-ai/ts-runtime/dist/db-errors.js
21923
21923
  var DbModuleNotInstalledError;
21924
21924
  var init_db_errors = __esm(() => {
21925
21925
  DbModuleNotInstalledError = class DbModuleNotInstalledError extends Error {
@@ -29130,7 +29130,7 @@ var init_execa = __esm(() => {
29130
29130
  } = getIpcExport());
29131
29131
  });
29132
29132
 
29133
- // ../../node_modules/.bun/@gobing-ai+ts-runtime@0.5.18+ea28d47622d670ff/node_modules/@gobing-ai/ts-runtime/dist/process-group.js
29133
+ // ../../node_modules/.bun/@gobing-ai+ts-runtime@0.5.21+44edd5c4d6372504/node_modules/@gobing-ai/ts-runtime/dist/process-group.js
29134
29134
  function resolveDeadline(optionTimeout, defaultTimeout) {
29135
29135
  if (optionTimeout === null)
29136
29136
  return null;
@@ -29222,7 +29222,7 @@ function ownProcessGroupLifecycle(ownership) {
29222
29222
  }
29223
29223
  var MAX_TIMEOUT_MS = 2147483647, DEFAULT_KILL_GRACE_MS = 5000, GROUP_POLL_INTERVAL_MS = 25, GROUP_KILL_PATIENCE_MS = 2000;
29224
29224
 
29225
- // ../../node_modules/.bun/@gobing-ai+ts-runtime@0.5.18+ea28d47622d670ff/node_modules/@gobing-ai/ts-runtime/dist/process-executor.js
29225
+ // ../../node_modules/.bun/@gobing-ai+ts-runtime@0.5.21+44edd5c4d6372504/node_modules/@gobing-ai/ts-runtime/dist/process-executor.js
29226
29226
  import { isatty } from "tty";
29227
29227
  function processEventSeverity(reason, exitCode) {
29228
29228
  if (reason === "error")
@@ -29594,7 +29594,7 @@ var init_process_executor = __esm(() => {
29594
29594
  init_config();
29595
29595
  });
29596
29596
 
29597
- // ../../node_modules/.bun/@gobing-ai+ts-runtime@0.5.18+ea28d47622d670ff/node_modules/@gobing-ai/ts-runtime/dist/runtime-node-bun.js
29597
+ // ../../node_modules/.bun/@gobing-ai+ts-runtime@0.5.21+44edd5c4d6372504/node_modules/@gobing-ai/ts-runtime/dist/runtime-node-bun.js
29598
29598
  function getNodeFileSystem() {
29599
29599
  if (!_nodeFileSystem)
29600
29600
  _nodeFileSystem = createNodeFileSystem();
@@ -29747,11 +29747,11 @@ var PluginManifestSchema = exports_external.looseObject({
29747
29747
  description: exports_external.string().optional(),
29748
29748
  operations: exports_external.record(exports_external.string(), PluginOperationSchema).optional()
29749
29749
  });
29750
- // ../../node_modules/.bun/@gobing-ai+ts-runtime@0.5.18+ea28d47622d670ff/node_modules/@gobing-ai/ts-runtime/dist/index.js
29750
+ // ../../node_modules/.bun/@gobing-ai+ts-runtime@0.5.21+44edd5c4d6372504/node_modules/@gobing-ai/ts-runtime/dist/index.js
29751
29751
  init_file_system_node();
29752
29752
  init_config();
29753
29753
 
29754
- // ../../node_modules/.bun/@gobing-ai+ts-runtime@0.5.18+ea28d47622d670ff/node_modules/@gobing-ai/ts-runtime/dist/path.js
29754
+ // ../../node_modules/.bun/@gobing-ai+ts-runtime@0.5.21+44edd5c4d6372504/node_modules/@gobing-ai/ts-runtime/dist/path.js
29755
29755
  function normalizeSeparators(path6) {
29756
29756
  return path6.replaceAll("\\", "/");
29757
29757
  }
@@ -29809,7 +29809,7 @@ function getProcessCwd() {
29809
29809
  return globalThis.process?.cwd?.() ?? "/";
29810
29810
  }
29811
29811
 
29812
- // ../../node_modules/.bun/@gobing-ai+ts-runtime@0.5.18+ea28d47622d670ff/node_modules/@gobing-ai/ts-runtime/dist/index.js
29812
+ // ../../node_modules/.bun/@gobing-ai+ts-runtime@0.5.21+44edd5c4d6372504/node_modules/@gobing-ai/ts-runtime/dist/index.js
29813
29813
  init_runtime_node_bun();
29814
29814
 
29815
29815
  // ../../packages/kk-core/src/render-md.ts
@@ -29925,7 +29925,7 @@ async function trySelectors(page, selectors, options = {}) {
29925
29925
  // ../../packages/publish-harness/src/browser.ts
29926
29926
  import { resolve as resolve2 } from "path";
29927
29927
 
29928
- // ../../node_modules/.bun/@gobing-ai+ts-browser-automation@0.5.18+1f5782d4d327a230/node_modules/@gobing-ai/ts-browser-automation/dist/errors.js
29928
+ // ../../node_modules/.bun/@gobing-ai+ts-browser-automation@0.5.21+b6949ba92ac553f0/node_modules/@gobing-ai/ts-browser-automation/dist/errors.js
29929
29929
  class BrowserAutomationError extends Error {
29930
29930
  constructor(message, options) {
29931
29931
  super(message, options);
@@ -29974,10 +29974,10 @@ class OperationAbortedError extends BrowserAutomationError {
29974
29974
  this.name = "OperationAbortedError";
29975
29975
  }
29976
29976
  }
29977
- // ../../node_modules/.bun/@gobing-ai+ts-browser-automation@0.5.18+1f5782d4d327a230/node_modules/@gobing-ai/ts-browser-automation/dist/launcher.js
29977
+ // ../../node_modules/.bun/@gobing-ai+ts-browser-automation@0.5.21+b6949ba92ac553f0/node_modules/@gobing-ai/ts-browser-automation/dist/launcher.js
29978
29978
  import { mkdir } from "fs/promises";
29979
29979
 
29980
- // ../../node_modules/.bun/@gobing-ai+ts-browser-automation@0.5.18+1f5782d4d327a230/node_modules/@gobing-ai/ts-browser-automation/dist/profile-dir.js
29980
+ // ../../node_modules/.bun/@gobing-ai+ts-browser-automation@0.5.21+b6949ba92ac553f0/node_modules/@gobing-ai/ts-browser-automation/dist/profile-dir.js
29981
29981
  function resolveProfileDir(profileDir) {
29982
29982
  if (profileDir.trim() === "") {
29983
29983
  throw new InvalidProfileDirError;
@@ -29989,7 +29989,7 @@ function resolveProfileDir(profileDir) {
29989
29989
  return resolved;
29990
29990
  }
29991
29991
 
29992
- // ../../node_modules/.bun/@gobing-ai+ts-browser-automation@0.5.18+1f5782d4d327a230/node_modules/@gobing-ai/ts-browser-automation/dist/launcher.js
29992
+ // ../../node_modules/.bun/@gobing-ai+ts-browser-automation@0.5.21+b6949ba92ac553f0/node_modules/@gobing-ai/ts-browser-automation/dist/launcher.js
29993
29993
  var PROFILE_DIR_MODE = 448;
29994
29994
  var BUSY_PROFILE_PATTERN = /singleton|already in use|in use by another/i;
29995
29995
  var CHROMIUM_MISSING_PATTERN = /playwright install|executable doesn't exist/i;
@@ -30040,7 +30040,7 @@ function createPersistentLauncher(options = {}) {
30040
30040
  };
30041
30041
  }
30042
30042
 
30043
- // ../../node_modules/.bun/@gobing-ai+ts-browser-automation@0.5.18+1f5782d4d327a230/node_modules/@gobing-ai/ts-browser-automation/dist/operations.js
30043
+ // ../../node_modules/.bun/@gobing-ai+ts-browser-automation@0.5.21+b6949ba92ac553f0/node_modules/@gobing-ai/ts-browser-automation/dist/operations.js
30044
30044
  var DEFAULT_TIMEOUT_MS = 30000;
30045
30045
  var POLL_INTERVAL_MS = 250;
30046
30046
  var defaultLauncher;