@gobing-ai/knowledge-kit 0.1.7 → 0.1.9

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 (36) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/dist/board/collect/index.js +744 -1107
  3. package/dist/board/daily-voice/index.js +741 -107
  4. package/dist/board/itc/index.js +32 -6
  5. package/dist/index.js +1793 -103
  6. package/package.json +1 -1
  7. package/plugins/ingestions/horizon-ingest/dist/index.js +24 -1
  8. package/plugins/ingestions/horizon-ingest/src/index.ts +34 -0
  9. package/plugins/kk/config.example.yaml +8 -0
  10. package/plugins/kk/decisions/extract/ai-news.yaml +7 -1
  11. package/plugins/kk/plugin.json +1 -1
  12. package/plugins/kk/publish-presets/daily-ai-voice.json +123 -0
  13. package/plugins/kk/schedules/kk-daily-publish3-cn-peak.cron.tmpl +20 -0
  14. package/plugins/kk/schedules/kk-daily-publish3-overseas.cron.tmpl +17 -0
  15. package/plugins/kk/scripts/daily-stages.ts +664 -83
  16. package/plugins/kk/scripts/kk-workflow-stages.ts +11 -1
  17. package/plugins/kk/scripts/topic-boundary.json +18 -83
  18. package/plugins/kk/workflows/kk-collect.yaml +2 -0
  19. package/plugins/kk/workflows/kk-daily-ai-voice2.yaml +82 -8
  20. package/plugins/kk/workflows/kk-daily-ai-voice3.yaml +842 -0
  21. package/plugins/kk/workflows/kk-daily-publish3.yaml +282 -0
  22. package/plugins/publishings/podcast-pub/README.md +65 -0
  23. package/plugins/publishings/podcast-pub/dist/index.js +1709 -274
  24. package/plugins/publishings/podcast-pub/plugin.json +1 -1
  25. package/plugins/publishings/podcast-pub/src/backfill.ts +465 -0
  26. package/plugins/publishings/podcast-pub/src/index.ts +211 -34
  27. package/plugins/publishings/podcast-pub/src/microfeed-client.ts +32 -3
  28. package/plugins/publishings/podcast-pub/src/seo.ts +1081 -0
  29. package/plugins/publishings/podcast-pub/src/show-notes.ts +71 -6
  30. package/plugins/publishings/surfdash-pub/dist/index.js +66 -5
  31. package/plugins/publishings/surfdash-pub/src/index.ts +94 -10
  32. package/plugins/publishings/wechatmp-pub/dist/index.js +54 -30
  33. package/plugins/publishings/wechatmp-pub/dist/profile-check.js +8 -3
  34. package/plugins/publishings/wechatmp-pub/src/enrich.ts +58 -38
  35. package/plugins/publishings/wechatmp-pub/src/flow.ts +18 -1
  36. package/plugins/publishings/wechatmp-pub/src/profile-check.ts +7 -1
@@ -21,16 +21,10 @@ import { clickSelector, fillSelector, modalState, tryFindSelector, type WechatPa
21
21
  const DIALOG = '.weui-desktop-dialog:visible';
22
22
  const CONFIRM = `${DIALOG} button:has-text("确认"), ${DIALOG} button:has-text("确定")`;
23
23
  /**
24
- * Every settings row follows one shape (operator walk 2026-10-10): a row control that opens a dialog,
25
- * an action inside it, then 确定. The row's own label is listed alongside its `>` expander because both
26
- * dispatch the same handler; the first selector that resolves wins.
24
+ * Dialog entries measured 2026-10-21. Confirm labels belong to each dialog: 原创 uses 确定,
25
+ * 创作来源 uses 确认. A generic text match can hit an inert row heading instead of its opener.
27
26
  */
28
- const ROW_ORIGINAL = [
29
- '.setting-group__switch.js_original_apply.js_edit_ori',
30
- '.setting-group__switch.js_edit_ori',
31
- '.js_edit_ori',
32
- 'text=原创',
33
- ];
27
+ const ROW_ORIGINAL = ['#js_original .setting-group__content-info', '#js_original .setting-group__title'];
34
28
  // Measured 2026-10-13: the 赞-tipped rows are `.js_reward_open` / `.js_reward_allow_click` — the
35
29
  // old `.setting-group__switch.js_can_reward` no longer resolves. The 原创 dialog's own 赞赏
36
30
  // switch (`js_reward_switch`) must never be clicked by this row list (R3, drift task 0272).
@@ -42,9 +36,11 @@ const ROW_REWARD = [
42
36
  'text=赞赏',
43
37
  ];
44
38
  const ROW_COLLECTION = ['.allow_click_opr.js_article_tags_label', 'text=合集'];
45
- const ROW_URL = ['.allow_click_opr.js_url_checkbox', 'text=原文链接'];
46
- const ROW_CLAIM = ['#js_claim_source_area .frm_checkbox_label', 'text=创作来源'];
39
+ const ROW_URL = ['#js_article_url_area .js_article_url_allow_click'];
40
+ const ROW_CLAIM = ['#js_claim_source_area .allow_click_opr'];
47
41
  const DIALOG_CONFIRM = CONFIRM;
42
+ const ORIGINAL_CONFIRM = `${DIALOG} button:text-is("确定")`;
43
+ const CLAIM_CONFIRM = `${DIALOG} button:text-is("确认")`;
48
44
  const TAG_INPUT = 'input[placeholder*="合集"]';
49
45
  /** The 赞赏 dialog's account field, filled only when the platform leaves it empty. */
50
46
  const REWARD_ACCOUNT = [
@@ -53,15 +49,23 @@ const REWARD_ACCOUNT = [
53
49
  `${DIALOG} input.frm_input`,
54
50
  ];
55
51
  /**
56
- * The 原文链接 dialog's URL field.
57
- * `URL_INPUT[0]` doubles as the committed-readback locator, so it must be concrete (not `:visible`).
52
+ * 原文链接 is a popover outside the dialog tree (measured 2026-10-21, task 0318).
53
+ * Its field disappears on commit; committed readback belongs to the settings row.
58
54
  */
59
- const URL_INPUT = [`${DIALOG} input[type="url"]`, `${DIALOG} input[placeholder*="链接"]`, `${DIALOG} input.frm_input`];
55
+ const URL_POPOVER = '.popover_inner:has(.popover-article-setting__content):visible';
56
+ const URL_INPUT = [`${URL_POPOVER} input.js_url[name="source_url"]`];
57
+ const URL_CONFIRM = `${URL_POPOVER} a.jsPopoverBt:text-is("确定")`;
58
+ const URL_CANCEL = `${URL_POPOVER} a.jsPopoverBt:text-is("取消")`;
59
+ const URL_COMMITTED = `(() => {
60
+ const area = document.querySelector('#js_article_url_area');
61
+ const checkbox = area?.querySelector('input.js_url_checkbox');
62
+ return checkbox?.checked === true ? area.querySelector('.article_url_setting')?.textContent ?? null : null;
63
+ })()`;
60
64
  /** The 原创 dialog's declaration radio (measured 2026-10-13): the platform requires an explicit
61
65
  * declaration type + the agreement check before 确定. `无需声明` is the undo choice, never a target. */
62
66
  const ORIGINAL_DECLARATION = `${DIALOG} label:has(.js_original_type_radio):has-text("文字原创")`;
63
- /** The 原创 agreement checkbox (measured 2026-10-13); absent on some account states — then 确定 speaks. */
64
- const ORIGINAL_AGREEMENT = `${DIALOG} .original_agreement`;
67
+ /** Drive the hidden checkbox through its label, preserving a pre-checked agreement (probe 6). */
68
+ const ORIGINAL_AGREEMENT = `${DIALOG} label.original_agreement:has(input[type="checkbox"]:not(:checked))`;
65
69
  /** The 合集 picker's options load after the dialog opens (the pre-ADR-029 settle, kept). */
66
70
  const COLLECTION_SETTLE_MS = 1_200;
67
71
  /** The settings panel re-renders after a dialog confirm and can briefly detach rows. */
@@ -86,9 +90,11 @@ function norm(value: unknown): string {
86
90
  * Playwright-only syntax (`:visible`, `:has-text`) never enters `querySelector` — it throws there.
87
91
  */
88
92
  const COMMITTED = {
89
- /** 原创 enabled = the settings row's checkbox checked (`#js_original` / `.js_ori_setting_checkbox`). */
93
+ /** Both undeclared and declared templates keep hidden inputs. Read the switch in the visible
94
+ * row branch, never the hidden undeclared template or the dialog's matching checkbox. */
90
95
  original: `(() => {
91
- const el = document.querySelector('#js_original, .js_ori_setting_checkbox');
96
+ const el = [...document.querySelectorAll('#js_original input.weui-desktop-switch__input')]
97
+ .find(input => input.parentElement?.getClientRects().length > 0);
92
98
  return el ? el.checked === true : null;
93
99
  })()`,
94
100
  /** 赞赏 enabled = the reward settings checkbox checked (area-scoped, else the visible cell's). */
@@ -133,7 +139,7 @@ export async function enrichArticle(page: WechatPage, meta: Record<string, unkno
133
139
  const on = (key: string) => meta[key] === true;
134
140
  // Sweep any leaked dialog so the next step (especially 保存为草稿) is never overlay-blocked.
135
141
  const sweep = async (): Promise<'closed' | 'unknown'> => {
136
- const cancel = await trySelectors(page, ['button:has-text("取消")']);
142
+ const cancel = await trySelectors(page, ['button:has-text("取消")', URL_CANCEL]);
137
143
  if (cancel.found) {
138
144
  try {
139
145
  await page
@@ -222,7 +228,7 @@ export async function enrichArticle(page: WechatPage, meta: Record<string, unkno
222
228
  if ((await readChecked(page, COMMITTED.original)) !== true) {
223
229
  const outcome = await step(name, async () => {
224
230
  await openRow(ROW_ORIGINAL, '原创声明');
225
- // Measured 2026-10-13: pick the 文字原创 radio, check the agreement when it renders,
231
+ // Measured 2026-10-21: keep the 文字原创 radio, click the agreement's label when it renders,
226
232
  // then 确定. The plain-text probe the old rail used could hit the dialog heading.
227
233
  await clickSelector(page, ORIGINAL_DECLARATION, 5_000);
228
234
  // trySelectors absorbs its own probe failures (returns found:false) — no .catch here.
@@ -230,7 +236,7 @@ export async function enrichArticle(page: WechatPage, meta: Record<string, unkno
230
236
  if (agreement.found) {
231
237
  await clickSelector(page, ORIGINAL_AGREEMENT, 3_000);
232
238
  }
233
- await clickSelector(page, DIALOG_CONFIRM, 8_000);
239
+ await clickSelector(page, ORIGINAL_CONFIRM, 8_000);
234
240
  });
235
241
  if (
236
242
  outcome !== 'capability' &&
@@ -305,27 +311,36 @@ export async function enrichArticle(page: WechatPage, meta: Record<string, unkno
305
311
  const originalUrl = str('originalUrl');
306
312
  if (originalUrl !== '') {
307
313
  const name = '原文链接';
314
+ const readUrl = async () => {
315
+ try {
316
+ return norm(await page.evaluate(URL_COMMITTED));
317
+ } catch {
318
+ return '';
319
+ }
320
+ };
308
321
  // Measured 2026-10-13: some accounts no longer render the 原文链接 row at all. That is an
309
322
  // unavailable capability — report it once, never fall back to the composer's unrelated
310
323
  // `链接` row (`btm-card-link__switch`).
311
- const urlRow = await tryFindSelector(page, ROW_URL, 2_000);
324
+ const urlRow =
325
+ (await readUrl()) === norm(originalUrl) ? 'committed' : await tryFindSelector(page, ROW_URL, 2_000);
312
326
  if (urlRow === undefined) {
313
327
  warnings.push(`${name}: 行不存在(当前公众号不支持),请手动处理或移除 originalUrl`);
314
- } else {
328
+ } else if (urlRow !== 'committed') {
315
329
  await step(name, async () => {
330
+ if ((await readUrl()) === norm(originalUrl)) return;
316
331
  await openRow(ROW_URL, '原文链接');
317
332
  const field = await trySelectors(page, URL_INPUT);
318
- if (!field.found) throw new Error('no URL field in the 原文链接 dialog');
333
+ if (!field.found) throw new Error('no URL field in the 原文链接 popover');
319
334
  await fillSelector(page, field.selector ?? URL_INPUT.join(', '), originalUrl);
320
- await clickSelector(page, DIALOG_CONFIRM, 8_000);
335
+ await clickSelector(page, URL_CONFIRM, 8_000);
336
+ // URL validation closes the popover asynchronously. Sweeping before it closes
337
+ // cancels that pending commit and turns a valid URL into a false mismatch.
338
+ await page.locator(URL_POPOVER).first().waitFor?.({ state: 'hidden', timeout: 8_000 });
321
339
  });
322
- await residual(name);
323
- // Only a readable committed URL equal to the requested trimmed value counts; the
324
- // composer `链接` row is never consulted.
325
- const locator = page.locator(URL_INPUT[0] ?? '').first();
326
- const committed =
327
- typeof locator.inputValue === 'function' ? await locator.inputValue().catch(() => null) : null;
328
- if (norm(committed) !== norm(originalUrl)) warnings.push(`${name}: 已提交但未读取到已保存的链接,请核对`);
340
+ if ((await readUrl()) !== norm(originalUrl)) warnings.push(`${name}: 已提交但未读取到已保存的链接,请核对`);
341
+ // Popovers are outside modalState's dialog inventory; release one even on a moved field
342
+ // or refused confirmation, before later settings and the draft save.
343
+ if ((await trySelectors(page, [URL_POPOVER])).found) await sweep();
329
344
  }
330
345
  }
331
346
 
@@ -343,17 +358,22 @@ export async function enrichArticle(page: WechatPage, meta: Record<string, unkno
343
358
  return null;
344
359
  }
345
360
  };
361
+ let unavailable = false;
346
362
  if ((await claimMatched()) !== true) {
347
- await step(name, async () => {
363
+ const outcome = await step(name, async () => {
348
364
  await openRow(ROW_CLAIM, '创作来源');
349
- const dialog = await waitForVisible(page, DIALOG, 3_000, { required: false });
350
- if (dialog !== undefined) {
351
- await clickSelector(page, `${DIALOG} :text("${claimSource}")`, 8_000);
352
- await clickSelector(page, DIALOG_CONFIRM, 8_000);
365
+ await waitForVisible(page, DIALOG, 3_000);
366
+ const stance = `${DIALOG} label:has(input[type="radio"]):has(:text-is(${JSON.stringify(claimSource)}))`;
367
+ if ((await tryFindSelector(page, [stance], 1_500)) === undefined) {
368
+ await sweep();
369
+ throw new CapabilityError(`平台未提供声明类型「${claimSource}」,已跳过`);
353
370
  }
371
+ await clickSelector(page, stance, 8_000);
372
+ await clickSelector(page, CLAIM_CONFIRM, 8_000);
354
373
  });
374
+ unavailable = outcome === 'capability';
355
375
  }
356
- if ((await claimMatched()) !== true && !(await residual(name))) {
376
+ if (!unavailable && (await claimMatched()) !== true && !(await residual(name))) {
357
377
  warnings.push(`${name}: 未生效,请手动设置`);
358
378
  }
359
379
  }
@@ -210,12 +210,25 @@ const AUDIO_UPLOAD_INPUT = [
210
210
  '.weui-desktop-dialog input[type="file"][accept*=".mp3"]',
211
211
  '.audio_dialog input[type="file"][accept*="audio"]',
212
212
  'input[type="file"][accept*=".mp3"]',
213
+ // 2026-10-21 measured: this build's upload form exposes a plain `input[type=file]` with NO accept
214
+ // attribute, so every accept-qualified candidate above missed and the run reported `音频: no audio
215
+ // file input appeared` — which costs the draft its audio player, the audio material's cover, and
216
+ // the library cover the 封面 step reuses. Dialog-scoped on purpose: the editor's own image input
217
+ // lives outside `.weui-desktop-dialog`, so it can never be the winner here.
218
+ '.weui-desktop-dialog input[type="file"]:not([accept*="image"])',
219
+ '.audio_dialog input[type="file"]:not([accept*="image"])',
213
220
  ];
214
221
  const UPLOAD_COVER = ['text=上传封面', 'button:has-text("上传封面")'];
215
222
  const AUDIO_COVER_INPUT = [
216
223
  '.weui-desktop-dialog input[type="file"][accept*="image/"]',
217
224
  '.audio_dialog input[type="file"][accept*="image/"]',
218
225
  'input[type="file"][accept*=".png"]',
226
+ // 2026-10-21 measured: the upload form's cover input carries `image/bmp, image/png, image/jpeg,
227
+ // image/jpg, image/gif, image/webp` — space-separated MIME names WITHOUT a slash-terminated
228
+ // `image/` prefix match (`accept*="image/"` misses `image/bmp`), so the cover upload silently
229
+ // skipped on every run and the draft shipped with no cover.
230
+ '.weui-desktop-dialog input[type="file"][accept*="image"]',
231
+ '.audio_dialog input[type="file"][accept*="image"]',
219
232
  ];
220
233
  const AUDIO_TITLE_INPUT = [
221
234
  '.weui-desktop-dialog input[placeholder*="标题"]',
@@ -585,7 +598,11 @@ async function applyLayout(page: WechatPage, deps: ModeRunDeps): Promise<string
585
598
  }
586
599
  const option = await tryFindSelector(page, LAYOUT_TARGET, 5_000);
587
600
  if (option === undefined) {
588
- return '一键排版: clicked, but no 匹配目标格式 option appeared — the body may be unformatted; check the draft';
601
+ // Measured 2026-10-21 with a body in the editor: `div#ai_layout_container > div.ailayout-btn_wrp`
602
+ // is present and clickable, and no option set ever renders (only a hidden `ailayout-edu_tips`
603
+ // onboarding block). Nothing programmable is left to click, so name the finding instead of
604
+ // guessing at the body's formatting.
605
+ return '一键排版: the control exists but this build renders no 匹配目标格式 option (measured 2026-10-21 with a body in place) — layout skipped, apply 一键排版 by hand before publishing';
589
606
  }
590
607
  try {
591
608
  await clickSelector(page, option, 5_000);
@@ -25,8 +25,14 @@ const LOGIN_TIMEOUT_MS = 180_000;
25
25
  * at that instant — so the poll must not spend all of it: a dead session then dies mid-report and
26
26
  * surfaces as `probe exit 1 (malformed probe output)` instead of `refused` (observed 2026-10-16, where
27
27
  * the 30s poll plus overhead overran `kk plugin check`'s 30s default).
28
+ *
29
+ * 25s, not 8s (2026-10-21): a dead session spends the WHOLE poll before it can report, so the reserve
30
+ * has to cover a cold `KK_BROWSER_CHANNEL=chrome` start, not a warm one — at 8s the first
31
+ * `kk plugin check wechatmp-pub xhs-pub --timeout 60000` of the day SIGKILLed the probe at 60s and
32
+ * reported `malformed probe output` while the immediately following run answered the real
33
+ * `refused — Authentication was not confirmed` in the same 60s budget.
28
34
  */
29
- const PROFILE_OVERHEAD_MS = 8_000;
35
+ const PROFILE_OVERHEAD_MS = 25_000;
30
36
  /** Floor for the readiness poll, so a tiny `--timeout` still probes the console instead of refusing blind. */
31
37
  const MIN_READINESS_MS = 1_000;
32
38