@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.
- package/.claude-plugin/marketplace.json +1 -1
- package/dist/board/collect/index.js +744 -1107
- package/dist/board/daily-voice/index.js +741 -107
- package/dist/board/itc/index.js +32 -6
- package/dist/index.js +1793 -103
- package/package.json +1 -1
- package/plugins/ingestions/horizon-ingest/dist/index.js +24 -1
- package/plugins/ingestions/horizon-ingest/src/index.ts +34 -0
- package/plugins/kk/config.example.yaml +8 -0
- package/plugins/kk/decisions/extract/ai-news.yaml +7 -1
- package/plugins/kk/plugin.json +1 -1
- package/plugins/kk/publish-presets/daily-ai-voice.json +123 -0
- package/plugins/kk/schedules/kk-daily-publish3-cn-peak.cron.tmpl +20 -0
- package/plugins/kk/schedules/kk-daily-publish3-overseas.cron.tmpl +17 -0
- package/plugins/kk/scripts/daily-stages.ts +664 -83
- package/plugins/kk/scripts/kk-workflow-stages.ts +11 -1
- package/plugins/kk/scripts/topic-boundary.json +18 -83
- package/plugins/kk/workflows/kk-collect.yaml +2 -0
- package/plugins/kk/workflows/kk-daily-ai-voice2.yaml +82 -8
- package/plugins/kk/workflows/kk-daily-ai-voice3.yaml +842 -0
- package/plugins/kk/workflows/kk-daily-publish3.yaml +282 -0
- package/plugins/publishings/podcast-pub/README.md +65 -0
- package/plugins/publishings/podcast-pub/dist/index.js +1709 -274
- package/plugins/publishings/podcast-pub/plugin.json +1 -1
- package/plugins/publishings/podcast-pub/src/backfill.ts +465 -0
- package/plugins/publishings/podcast-pub/src/index.ts +211 -34
- package/plugins/publishings/podcast-pub/src/microfeed-client.ts +32 -3
- package/plugins/publishings/podcast-pub/src/seo.ts +1081 -0
- package/plugins/publishings/podcast-pub/src/show-notes.ts +71 -6
- package/plugins/publishings/surfdash-pub/dist/index.js +66 -5
- package/plugins/publishings/surfdash-pub/src/index.ts +94 -10
- package/plugins/publishings/wechatmp-pub/dist/index.js +54 -30
- package/plugins/publishings/wechatmp-pub/dist/profile-check.js +8 -3
- package/plugins/publishings/wechatmp-pub/src/enrich.ts +58 -38
- package/plugins/publishings/wechatmp-pub/src/flow.ts +18 -1
- 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
|
-
*
|
|
25
|
-
*
|
|
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 = ['.
|
|
46
|
-
const ROW_CLAIM = ['#js_claim_source_area .
|
|
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
|
-
*
|
|
57
|
-
*
|
|
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
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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.
|
|
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-
|
|
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,
|
|
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 =
|
|
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 原文链接
|
|
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,
|
|
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
|
|
323
|
-
//
|
|
324
|
-
//
|
|
325
|
-
|
|
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
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
await
|
|
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
|
-
|
|
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 =
|
|
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
|
|