@gobing-ai/knowledge-kit 0.0.35 → 0.0.37
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/README.md +1 -1
- package/dist/board/collect/index.css +42 -0
- package/dist/board/collect/index.js +787 -0
- package/dist/board/daily-voice/index.css +13 -0
- package/dist/board/daily-voice/index.js +902 -0
- package/dist/board/itc/index.css +14 -0
- package/dist/board/itc/index.js +1026 -0
- package/dist/index.js +12996 -10476
- package/package.json +10 -9
- package/plugins/generations/content-gen/dist/index.js +1015 -10814
- package/plugins/generations/content-gen/package.json +1 -1
- package/plugins/generations/core-facts-gen/dist/index.js +1002 -10801
- package/plugins/generations/core-facts-gen/package.json +1 -1
- package/plugins/generations/daily-article-gen/dist/index.js +1004 -10803
- package/plugins/generations/daily-article-gen/package.json +1 -1
- package/plugins/generations/dailynews-gen/dist/index.js +1111 -10888
- package/plugins/generations/dailynews-gen/package.json +1 -1
- package/plugins/generations/dailynews-gen/src/script-plan.ts +32 -0
- package/plugins/generations/episode-plan-gen/dist/index.js +1124 -10923
- package/plugins/generations/episode-plan-gen/package.json +1 -1
- package/plugins/generations/gemini-tts/dist/index.js +1104 -10903
- package/plugins/generations/gemini-tts/package.json +1 -1
- package/plugins/generations/image-gen/dist/index.js +1239 -11035
- package/plugins/generations/image-gen/package.json +1 -1
- package/plugins/generations/news-report-gen/dist/index.js +1048 -10847
- package/plugins/generations/news-report-gen/package.json +1 -1
- package/plugins/generations/voice-gen/dist/index.js +1062 -10861
- package/plugins/generations/voice-gen/package.json +1 -1
- package/plugins/generations/voxcpm-tts/README.md +72 -0
- package/plugins/generations/voxcpm-tts/dist/index.js +14006 -0
- package/plugins/generations/voxcpm-tts/package.json +16 -0
- package/plugins/generations/voxcpm-tts/plugin.json +6 -0
- package/plugins/generations/voxcpm-tts/profiles.json +10 -0
- package/plugins/generations/voxcpm-tts/pyproject.toml +9 -0
- package/plugins/generations/voxcpm-tts/python/worker.py +353 -0
- package/plugins/generations/voxcpm-tts/src/index.ts +42 -0
- package/plugins/generations/voxcpm-tts/src/voxcpm-client.ts +765 -0
- package/plugins/generations/voxcpm-tts/tsconfig.json +8 -0
- package/plugins/generations/voxcpm-tts/uv.lock +2571 -0
- package/plugins/ingestions/aihot-ingest/dist/index.js +1081 -10628
- package/plugins/ingestions/aihot-ingest/package.json +1 -1
- package/plugins/ingestions/horizon-ingest/dist/index.js +1013 -10812
- package/plugins/ingestions/horizon-ingest/package.json +1 -1
- package/plugins/ingestions/karakeep-local/dist/index.js +689 -10774
- package/plugins/ingestions/karakeep-local/package.json +1 -1
- package/plugins/ingestions/last30days-ingest/dist/index.js +1011 -10810
- package/plugins/ingestions/last30days-ingest/package.json +1 -1
- package/plugins/ingestions/web-search/dist/index.js +1250 -11033
- package/plugins/ingestions/web-search/package.json +1 -1
- package/plugins/kk/README.md +29 -2
- package/plugins/kk/plugin.json +1 -1
- package/plugins/kk/schedules/cron.tmpl +9 -1
- package/plugins/kk/schedules/kk-serve.launchd.plist.tmpl +79 -0
- package/plugins/kk/schedules/launchd.plist.tmpl +11 -1
- package/plugins/kk/scripts/daily-stages.ts +428 -46
- package/plugins/kk/scripts/daily-voice-style.zh.md +1 -1
- package/plugins/kk/scripts/kk-workflow-stages.ts +76 -146
- package/plugins/kk/skills/publish/SKILL.md +8 -4
- package/plugins/kk/workflows/feature-verification.yaml +93 -0
- package/plugins/kk/workflows/judge-gated-publish-example.yaml +1 -0
- package/plugins/kk/workflows/kk-collect.yaml +43 -11
- package/plugins/kk/workflows/kk-daily-ai-voice.yaml +4 -2
- package/plugins/kk/workflows/kk-daily-ai-voice2.yaml +87 -16
- package/plugins/kk/workflows/kk-daily-safety.yaml +15 -2
- package/plugins/kk/workflows/kk-itc.yaml +8 -1
- package/plugins/kk/workflows/kk-solo-podcast.yaml +2 -0
- package/plugins/publishings/emdash-pub/dist/index.js +987 -10814
- package/plugins/publishings/emdash-pub/package.json +1 -1
- package/plugins/publishings/infoq-pub/dist/index.js +1928 -11444
- package/plugins/publishings/infoq-pub/dist/profile-check.js +1677 -11199
- package/plugins/publishings/infoq-pub/package.json +1 -1
- package/plugins/publishings/infoq-pub/plugin.json +5 -1
- package/plugins/publishings/juejin-pub/dist/index.js +1940 -11456
- package/plugins/publishings/juejin-pub/dist/profile-check.js +1677 -11199
- package/plugins/publishings/juejin-pub/package.json +1 -1
- package/plugins/publishings/juejin-pub/plugin.json +5 -1
- package/plugins/publishings/medium-pub/dist/index.js +978 -10805
- package/plugins/publishings/medium-pub/package.json +1 -1
- package/plugins/publishings/podcast-pub/dist/index.js +1016 -10843
- package/plugins/publishings/podcast-pub/package.json +1 -1
- package/plugins/publishings/qiita-pub/dist/index.js +976 -10803
- package/plugins/publishings/qiita-pub/package.json +1 -1
- package/plugins/publishings/substack-pub/dist/index.js +1932 -11448
- package/plugins/publishings/substack-pub/dist/profile-check.js +1698 -11200
- package/plugins/publishings/substack-pub/package.json +1 -1
- package/plugins/publishings/substack-pub/plugin.json +4 -1
- package/plugins/publishings/substack-pub/src/profile-check.ts +36 -3
- package/plugins/publishings/surfdash-pub/dist/index.js +1151 -10834
- package/plugins/publishings/surfdash-pub/package.json +1 -1
- package/plugins/publishings/surfdash-pub/src/index.ts +137 -15
- package/plugins/publishings/surfdash-pub/src/patch.ts +144 -0
- package/plugins/publishings/threads-pub/dist/index.js +12394 -0
- package/plugins/publishings/threads-pub/package.json +17 -0
- package/plugins/publishings/threads-pub/plugin.json +7 -0
- package/plugins/publishings/threads-pub/src/index.ts +340 -0
- package/plugins/publishings/threads-pub/tsconfig.json +4 -0
- package/plugins/publishings/wechatmp-pub/dist/index.js +2508 -11695
- package/plugins/publishings/wechatmp-pub/dist/profile-check.js +1656 -11188
- package/plugins/publishings/wechatmp-pub/package.json +1 -1
- package/plugins/publishings/wechatmp-pub/plugin.json +5 -1
- package/plugins/publishings/wechatmp-pub/src/enrich.ts +244 -71
- package/plugins/publishings/wechatmp-pub/src/flow.ts +412 -96
- package/plugins/publishings/wechatmp-pub/src/index.ts +24 -3
- package/plugins/publishings/wechatmp-pub/src/page.ts +16 -0
- package/plugins/publishings/wechatmp-pub/src/profile-check.ts +40 -4
- package/plugins/publishings/weibo-pub/dist/index.js +21345 -0
- package/plugins/publishings/weibo-pub/dist/profile-check.js +20817 -0
- package/plugins/publishings/weibo-pub/package.json +19 -0
- package/plugins/publishings/weibo-pub/plugin.json +12 -0
- package/plugins/publishings/weibo-pub/src/index.ts +868 -0
- package/plugins/publishings/weibo-pub/src/profile-check.ts +20 -0
- package/plugins/publishings/weibo-pub/tsconfig.json +4 -0
- package/plugins/publishings/x-pub/dist/index.js +1948 -11440
- package/plugins/publishings/x-pub/dist/profile-check.js +1677 -11199
- package/plugins/publishings/x-pub/package.json +1 -1
- package/plugins/publishings/x-pub/plugin.json +5 -1
- package/plugins/publishings/xhs-pub/dist/index.js +1993 -11509
- package/plugins/publishings/xhs-pub/dist/profile-check.js +1697 -11212
- package/plugins/publishings/xhs-pub/package.json +1 -1
- package/plugins/publishings/xhs-pub/plugin.json +5 -1
- package/plugins/publishings/xhs-pub/src/profile-check.ts +72 -2
- package/plugins/publishings/zenn-pub/dist/index.js +988 -10815
- package/plugins/publishings/zenn-pub/package.json +1 -1
|
@@ -4,5 +4,9 @@
|
|
|
4
4
|
"entry": "./dist/index.js",
|
|
5
5
|
"check": "./dist/profile-check.js",
|
|
6
6
|
"version": "1.0.0",
|
|
7
|
-
"description": "Publish plugin for the WeChat MP console via ts-browser-automation: draft-box first (ADR-022), headed publish behind a session preflight"
|
|
7
|
+
"description": "Publish plugin for the WeChat MP console via ts-browser-automation: draft-box first (ADR-022), headed publish behind a session preflight",
|
|
8
|
+
"browserSession": {
|
|
9
|
+
"profileName": "wechatmp",
|
|
10
|
+
"homeUrl": "https://mp.weixin.qq.com/"
|
|
11
|
+
}
|
|
8
12
|
}
|
|
@@ -1,19 +1,21 @@
|
|
|
1
1
|
import { trySelectors } from '@gobing-ai/publish-harness';
|
|
2
|
-
import { clickSelector, fillSelector, type WechatPage, waitForVisible } from './page';
|
|
2
|
+
import { clickSelector, fillSelector, modalState, tryFindSelector, type WechatPage, waitForVisible } from './page';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* The 文章 operation's `metadata.wechatmp` enrichment rail (2026-09-29, ADR-026
|
|
5
|
+
* The 文章 operation's `metadata.wechatmp` enrichment rail (2026-09-29, ADR-026; committed-value
|
|
6
|
+
* contract reworked in task 0260).
|
|
6
7
|
*
|
|
7
8
|
* The daily MP stage wires operator preferences into `metadata.wechatmp` (原创声明 + 作者, 合集,
|
|
8
9
|
* AI 创作来源, audio embed) and this module drives the v2 editor's settings panel for them. Every
|
|
9
|
-
* step
|
|
10
|
-
*
|
|
11
|
-
*
|
|
10
|
+
* step reads the REQUESTED committed editor state BEFORE acting (an already-satisfied setting is
|
|
11
|
+
* never toggled) and again AFTER its dialog closes; only the exact requested committed value
|
|
12
|
+
* establishes success. Missing, throwing or unverifiable probes are named warnings — never truthy
|
|
13
|
+
* defaults, and account/capability refusals (赞赏账户, 原文链接 row) warn ONCE without retry.
|
|
12
14
|
*
|
|
13
15
|
* What is deliberately NOT here: the cover. The panel has NO file input (discovery §5.2) — the
|
|
14
16
|
* pre-ADR-029 cover step uploaded the file into the editor's *image* input, i.e. into the body. The
|
|
15
|
-
* cover is now the
|
|
16
|
-
* is attached as the first body image there.
|
|
17
|
+
* cover is now the `从正文选择`/图片库 step over identified assets (`./flow`), and
|
|
18
|
+
* `metadata.wechatmp.coverPath` is attached as the first body image there.
|
|
17
19
|
*/
|
|
18
20
|
|
|
19
21
|
const DIALOG = '.weui-desktop-dialog:visible';
|
|
@@ -23,8 +25,22 @@ const CONFIRM = `${DIALOG} button:has-text("确认"), ${DIALOG} button:has-text(
|
|
|
23
25
|
* an action inside it, then 确定. The row's own label is listed alongside its `>` expander because both
|
|
24
26
|
* dispatch the same handler; the first selector that resolves wins.
|
|
25
27
|
*/
|
|
26
|
-
const ROW_ORIGINAL = [
|
|
27
|
-
|
|
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
|
+
];
|
|
34
|
+
// Measured 2026-10-13: the 赞-tipped rows are `.js_reward_open` / `.js_reward_allow_click` — the
|
|
35
|
+
// old `.setting-group__switch.js_can_reward` no longer resolves. The 原创 dialog's own 赞赏
|
|
36
|
+
// switch (`js_reward_switch`) must never be clicked by this row list (R3, drift task 0272).
|
|
37
|
+
const ROW_REWARD = [
|
|
38
|
+
'.setting-group__switch.js_reward_open',
|
|
39
|
+
'.js_reward_open',
|
|
40
|
+
'.js_reward_allow_click',
|
|
41
|
+
'.setting-group__switch.js_can_reward',
|
|
42
|
+
'text=赞赏',
|
|
43
|
+
];
|
|
28
44
|
const ROW_COLLECTION = ['.allow_click_opr.js_article_tags_label', 'text=合集'];
|
|
29
45
|
const ROW_URL = ['.allow_click_opr.js_url_checkbox', 'text=原文链接'];
|
|
30
46
|
const ROW_CLAIM = ['#js_claim_source_area .frm_checkbox_label', 'text=创作来源'];
|
|
@@ -36,33 +52,87 @@ const REWARD_ACCOUNT = [
|
|
|
36
52
|
`${DIALOG} input[placeholder*="账户"]`,
|
|
37
53
|
`${DIALOG} input.frm_input`,
|
|
38
54
|
];
|
|
39
|
-
/**
|
|
55
|
+
/**
|
|
56
|
+
* The 原文链接 dialog's URL field.
|
|
57
|
+
* `URL_INPUT[0]` doubles as the committed-readback locator, so it must be concrete (not `:visible`).
|
|
58
|
+
*/
|
|
40
59
|
const URL_INPUT = [`${DIALOG} input[type="url"]`, `${DIALOG} input[placeholder*="链接"]`, `${DIALOG} input.frm_input`];
|
|
41
|
-
/** The 原创 dialog's declaration
|
|
42
|
-
|
|
60
|
+
/** The 原创 dialog's declaration radio (measured 2026-10-13): the platform requires an explicit
|
|
61
|
+
* declaration type + the agreement check before 确定. `无需声明` is the undo choice, never a target. */
|
|
62
|
+
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`;
|
|
43
65
|
/** The 合集 picker's options load after the dialog opens (the pre-ADR-029 settle, kept). */
|
|
44
66
|
const COLLECTION_SETTLE_MS = 1_200;
|
|
45
67
|
/** The settings panel re-renders after a dialog confirm and can briefly detach rows. */
|
|
46
68
|
const STEP_RETRY_SETTLE_MS = 1_000;
|
|
47
69
|
const SWEEP_SETTLE_MS = 400;
|
|
48
70
|
|
|
71
|
+
/**
|
|
72
|
+
* A capability/account refusal the next run cannot fix by retrying (no 赏账户, no 原文链接 row).
|
|
73
|
+
* `step` never retries one of these — the warning goes out once.
|
|
74
|
+
*/
|
|
75
|
+
class CapabilityError extends Error {}
|
|
76
|
+
|
|
77
|
+
/** trim + collapse internal whitespace — the matching normalization for every text/value readback. */
|
|
78
|
+
function norm(value: unknown): string {
|
|
79
|
+
return typeof value === 'string' ? value.replace(/\s+/g, ' ').trim() : '';
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Native-CSS committed-state probes. Runtime DOM properties only (a checked state read from the
|
|
84
|
+
* HTML attribute would see the template, not the editor) — and `null` when the shape is absent, so
|
|
85
|
+
* an unobservable state can never read as "satisfied".
|
|
86
|
+
* Playwright-only syntax (`:visible`, `:has-text`) never enters `querySelector` — it throws there.
|
|
87
|
+
*/
|
|
88
|
+
const COMMITTED = {
|
|
89
|
+
/** 原创 enabled = the settings row's checkbox checked (`#js_original` / `.js_ori_setting_checkbox`). */
|
|
90
|
+
original: `(() => {
|
|
91
|
+
const el = document.querySelector('#js_original, .js_ori_setting_checkbox');
|
|
92
|
+
return el ? el.checked === true : null;
|
|
93
|
+
})()`,
|
|
94
|
+
/** 赞赏 enabled = the reward settings checkbox checked (area-scoped, else the visible cell's). */
|
|
95
|
+
reward: `(() => {
|
|
96
|
+
const el = document.querySelector('#js_reward_setting_area .js_reward_setting_checkbox') ||
|
|
97
|
+
document.querySelector('.js_reward_allow_click_cell .js_reward_setting');
|
|
98
|
+
return el ? el.checked === true : null;
|
|
99
|
+
})()`,
|
|
100
|
+
} as const;
|
|
101
|
+
|
|
102
|
+
/** The claim probe's JSON answer: `{ enabled, matched }` or bare `null` (unobservable). */
|
|
103
|
+
const CLAIM_COMMITTED = `(() => {
|
|
104
|
+
const area = document.querySelector('#js_claim_source_area');
|
|
105
|
+
if (!area) return null;
|
|
106
|
+
const label = area.querySelector('.js_claim_source_selected');
|
|
107
|
+
const text = (label ? (label.textContent ?? '') : '').replace(/\\s+/g, ' ').trim();
|
|
108
|
+
const checkbox = area.querySelector('.js_claim_source, input[type=checkbox]');
|
|
109
|
+
const enabled = checkbox ? checkbox.checked === true : null;
|
|
110
|
+
return { enabled, text };
|
|
111
|
+
})()`;
|
|
112
|
+
|
|
113
|
+
/** Read a settings checkbox's committed enabled state; `null` = unobservable (never "satisfied"). */
|
|
114
|
+
async function readChecked(page: WechatPage, expression: string): Promise<boolean | null> {
|
|
115
|
+
try {
|
|
116
|
+
const raw = await page.evaluate(expression);
|
|
117
|
+
return typeof raw === 'boolean' ? raw : null;
|
|
118
|
+
} catch {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
49
123
|
/**
|
|
50
124
|
* Set the operator's draft metadata (原创/赞赏/合集/原文链接/创作来源) in the v2 editor.
|
|
51
125
|
*
|
|
52
|
-
* Runs on a FRESH draft only —
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
* Every step follows the operator's measured shape (2026-10-10): open the row, act in the dialog,
|
|
56
|
-
* click 确定. The previous rail modelled 原创 as "choose the declaration, then the author must resolve
|
|
57
|
-
* to the 赏 account" and skipped 赞赏 / 原文链接 entirely — which is why 作者, 原创 and 赞赏 all came
|
|
58
|
-
* back unset on the daily run.
|
|
126
|
+
* Runs on a FRESH draft only — unrequested toggles are never turned off, but the toggles are not
|
|
127
|
+
* otherwise state-aware for re-runs beyond the committed pre-read. Per-field failures are collected
|
|
128
|
+
* into warnings and surfaced in the `Result`; the draft still saves.
|
|
59
129
|
*/
|
|
60
130
|
export async function enrichArticle(page: WechatPage, meta: Record<string, unknown>): Promise<string[]> {
|
|
61
131
|
const warnings: string[] = [];
|
|
62
132
|
const str = (key: string) => (typeof meta[key] === 'string' ? (meta[key] as string) : '');
|
|
63
133
|
const on = (key: string) => meta[key] === true;
|
|
64
134
|
// Sweep any leaked dialog so the next step (especially 保存为草稿) is never overlay-blocked.
|
|
65
|
-
const sweep = async () => {
|
|
135
|
+
const sweep = async (): Promise<'closed' | 'unknown'> => {
|
|
66
136
|
const cancel = await trySelectors(page, ['button:has-text("取消")']);
|
|
67
137
|
if (cancel.found) {
|
|
68
138
|
try {
|
|
@@ -76,6 +146,22 @@ export async function enrichArticle(page: WechatPage, meta: Record<string, unkno
|
|
|
76
146
|
}
|
|
77
147
|
await page.press?.('Escape');
|
|
78
148
|
await page.waitForTimeout?.(SWEEP_SETTLE_MS);
|
|
149
|
+
const state = await modalState(page);
|
|
150
|
+
if (state === 'open') {
|
|
151
|
+
// One explicit close-button pass before giving up: some dialogs ignore Escape.
|
|
152
|
+
for (const close of ['.weui-desktop-dialog__close-btn', '.weui-desktop-dialog__close']) {
|
|
153
|
+
const control = await tryFindSelector(page, [close], 1_500);
|
|
154
|
+
if (control === undefined) continue;
|
|
155
|
+
try {
|
|
156
|
+
await clickSelector(page, control, 2_000);
|
|
157
|
+
} catch {
|
|
158
|
+
// Gone between the probe and the click.
|
|
159
|
+
}
|
|
160
|
+
break;
|
|
161
|
+
}
|
|
162
|
+
return (await modalState(page)) === 'closed' ? 'closed' : 'unknown';
|
|
163
|
+
}
|
|
164
|
+
return state === 'closed' ? 'closed' : 'unknown';
|
|
79
165
|
};
|
|
80
166
|
/** Open a settings row: its label or `>` expander, whichever the build exposes. */
|
|
81
167
|
const openRow = async (row: readonly string[], what: string) => {
|
|
@@ -90,63 +176,120 @@ export async function enrichArticle(page: WechatPage, meta: Record<string, unkno
|
|
|
90
176
|
}
|
|
91
177
|
throw new Error(`no ${what} row control among [${row.join(' | ')}]`);
|
|
92
178
|
};
|
|
93
|
-
/**
|
|
94
|
-
|
|
179
|
+
/**
|
|
180
|
+
* One bounded retry per step: act, and on a non-capability failure sweep, RE-READ the committed
|
|
181
|
+
* state, and retry once — a step that closed its dialog successfully on the sweep never clicks
|
|
182
|
+
* again. A capability refusal (`CapabilityError`) is reported ONCE here and the caller skips its
|
|
183
|
+
* post-read warning (the refusal is the outcome, not a missed commit).
|
|
184
|
+
*/
|
|
185
|
+
const step = async (name: string, fn: () => Promise<void>): Promise<'ok' | 'failed' | 'capability'> => {
|
|
95
186
|
try {
|
|
96
187
|
await fn();
|
|
97
|
-
return;
|
|
98
|
-
} catch {
|
|
188
|
+
return 'ok';
|
|
189
|
+
} catch (error) {
|
|
190
|
+
if (error instanceof CapabilityError) {
|
|
191
|
+
warnings.push(`${name}: ${error.message}`);
|
|
192
|
+
return 'capability';
|
|
193
|
+
}
|
|
99
194
|
// retry once below
|
|
100
195
|
}
|
|
101
196
|
await sweep();
|
|
102
197
|
await page.waitForTimeout?.(STEP_RETRY_SETTLE_MS);
|
|
103
198
|
try {
|
|
104
199
|
await fn();
|
|
200
|
+
return 'ok';
|
|
105
201
|
} catch (error) {
|
|
106
202
|
warnings.push(`${name}: ${(error instanceof Error ? error.message : String(error)).slice(0, 120)}`);
|
|
107
203
|
await sweep();
|
|
204
|
+
return 'failed';
|
|
108
205
|
}
|
|
109
206
|
};
|
|
110
|
-
/**
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
false;
|
|
207
|
+
/** The named warning for a dialog that is open or unreadable after a step — never "fine". */
|
|
208
|
+
const residual = async (name: string): Promise<boolean> => {
|
|
209
|
+
const state = await modalState(page);
|
|
210
|
+
if (state === 'closed') return false;
|
|
211
|
+
warnings.push(
|
|
212
|
+
state === 'open'
|
|
213
|
+
? `${name}: dialog did not close — check the draft`
|
|
214
|
+
: `${name}: dialog state unknown after cleanup — a residual overlay may block later steps`,
|
|
215
|
+
);
|
|
216
|
+
return true;
|
|
217
|
+
};
|
|
114
218
|
|
|
115
219
|
if (on('original')) {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
220
|
+
const name = '原创声明';
|
|
221
|
+
// Already committed: never open rows (and never toggle a satisfied setting off).
|
|
222
|
+
if ((await readChecked(page, COMMITTED.original)) !== true) {
|
|
223
|
+
const outcome = await step(name, async () => {
|
|
224
|
+
await openRow(ROW_ORIGINAL, '原创声明');
|
|
225
|
+
// Measured 2026-10-13: pick the 文字原创 radio, check the agreement when it renders,
|
|
226
|
+
// then 确定. The plain-text probe the old rail used could hit the dialog heading.
|
|
227
|
+
await clickSelector(page, ORIGINAL_DECLARATION, 5_000);
|
|
228
|
+
// trySelectors absorbs its own probe failures (returns found:false) — no .catch here.
|
|
229
|
+
const agreement = await trySelectors(page, [ORIGINAL_AGREEMENT]);
|
|
230
|
+
if (agreement.found) {
|
|
231
|
+
await clickSelector(page, ORIGINAL_AGREEMENT, 3_000);
|
|
232
|
+
}
|
|
233
|
+
await clickSelector(page, DIALOG_CONFIRM, 8_000);
|
|
234
|
+
});
|
|
235
|
+
if (
|
|
236
|
+
outcome !== 'capability' &&
|
|
237
|
+
(await readChecked(page, COMMITTED.original)) !== true &&
|
|
238
|
+
!(await residual(name))
|
|
239
|
+
) {
|
|
240
|
+
warnings.push(`${name}: 原创声明未生效,请手动设置`);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
124
243
|
}
|
|
125
244
|
|
|
126
245
|
if (on('reward')) {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
246
|
+
const name = '赞赏';
|
|
247
|
+
if ((await readChecked(page, COMMITTED.reward)) !== true) {
|
|
248
|
+
const outcome = await step(name, async () => {
|
|
249
|
+
await openRow(ROW_REWARD, '赞赏');
|
|
250
|
+
// Measured 2026-10-13: without a 赏账户 the dialog renders the 前往开通 link + a
|
|
251
|
+
// 无法开启-style prerequisite line instead of the account field — an operator action
|
|
252
|
+
// (开通赞赏), never a selector failure, and no retry of this run can change it.
|
|
253
|
+
const blocked = await tryFindSelector(
|
|
254
|
+
page,
|
|
255
|
+
[`${DIALOG} :has-text("前往开通")`, `${DIALOG} :has-text("无法开启")`],
|
|
256
|
+
1_500,
|
|
257
|
+
);
|
|
258
|
+
if (blocked !== undefined)
|
|
259
|
+
throw new CapabilityError('需要赞赏账户(运营者先在平台开通),已跳过自动填写');
|
|
260
|
+
// 赞赏账户 defaults from 作者; fill it only when the platform left it empty.
|
|
261
|
+
const author = str('author');
|
|
262
|
+
if (author !== '') {
|
|
263
|
+
for (const selector of REWARD_ACCOUNT) {
|
|
264
|
+
// trySelectors absorbs its own probe failures (returns found:false), so no
|
|
265
|
+
// extra .catch is needed here.
|
|
266
|
+
const found = await trySelectors(page, [selector]);
|
|
267
|
+
if (!found.found) continue;
|
|
268
|
+
// Locator read, not native querySelector: the expression must stay native CSS
|
|
269
|
+
// (Playwright's `:visible` throws inside `evaluate`, task 0260 AC4).
|
|
270
|
+
const locator = page.locator(selector).first();
|
|
271
|
+
if (typeof locator.inputValue !== 'function') break;
|
|
272
|
+
const empty = norm(await locator.inputValue().catch(() => null)) === '';
|
|
273
|
+
if (empty) await fillSelector(page, selector, author);
|
|
274
|
+
break;
|
|
275
|
+
}
|
|
140
276
|
}
|
|
277
|
+
await clickSelector(page, DIALOG_CONFIRM, 8_000);
|
|
278
|
+
});
|
|
279
|
+
if (
|
|
280
|
+
outcome !== 'capability' &&
|
|
281
|
+
(await readChecked(page, COMMITTED.reward)) !== true &&
|
|
282
|
+
!(await residual(name))
|
|
283
|
+
) {
|
|
284
|
+
warnings.push(`${name}: 赞赏未生效,请手动设置`);
|
|
141
285
|
}
|
|
142
|
-
|
|
143
|
-
});
|
|
144
|
-
if (!(await dialogsClosed())) warnings.push('赞赏: dialog did not close — check the draft');
|
|
286
|
+
}
|
|
145
287
|
}
|
|
146
288
|
|
|
147
289
|
const collection = str('collection');
|
|
148
290
|
if (collection !== '') {
|
|
149
|
-
|
|
291
|
+
const name = '合集';
|
|
292
|
+
await step(name, async () => {
|
|
150
293
|
await openRow(ROW_COLLECTION, '合集');
|
|
151
294
|
await waitForVisible(page, TAG_INPUT, 8_000);
|
|
152
295
|
await fillSelector(page, TAG_INPUT, collection);
|
|
@@ -154,35 +297,65 @@ export async function enrichArticle(page: WechatPage, meta: Record<string, unkno
|
|
|
154
297
|
await clickSelector(page, `${DIALOG} :text("${collection}")`, 8_000);
|
|
155
298
|
await clickSelector(page, DIALOG_CONFIRM, 8_000);
|
|
156
299
|
});
|
|
157
|
-
|
|
300
|
+
// 合集 keeps its existing behavior (no committed-value probe is shipped for the picker);
|
|
301
|
+
// only its cleanup is isolated here.
|
|
302
|
+
await residual(name);
|
|
158
303
|
}
|
|
159
304
|
|
|
160
305
|
const originalUrl = str('originalUrl');
|
|
161
306
|
if (originalUrl !== '') {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
307
|
+
const name = '原文链接';
|
|
308
|
+
// Measured 2026-10-13: some accounts no longer render the 原文链接 row at all. That is an
|
|
309
|
+
// unavailable capability — report it once, never fall back to the composer's unrelated
|
|
310
|
+
// `链接` row (`btm-card-link__switch`).
|
|
311
|
+
const urlRow = await tryFindSelector(page, ROW_URL, 2_000);
|
|
312
|
+
if (urlRow === undefined) {
|
|
313
|
+
warnings.push(`${name}: 行不存在(当前公众号不支持),请手动处理或移除 originalUrl`);
|
|
314
|
+
} else {
|
|
315
|
+
await step(name, async () => {
|
|
316
|
+
await openRow(ROW_URL, '原文链接');
|
|
317
|
+
const field = await trySelectors(page, URL_INPUT);
|
|
318
|
+
if (!field.found) throw new Error('no URL field in the 原文链接 dialog');
|
|
319
|
+
await fillSelector(page, field.selector ?? URL_INPUT.join(', '), originalUrl);
|
|
320
|
+
await clickSelector(page, DIALOG_CONFIRM, 8_000);
|
|
321
|
+
});
|
|
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}: 已提交但未读取到已保存的链接,请核对`);
|
|
329
|
+
}
|
|
170
330
|
}
|
|
171
331
|
|
|
172
332
|
// 创作来源 is the operator's chosen stance (`个人观点,仅供参考` for this column), not a fixed
|
|
173
333
|
// "AI generated" declaration — the label is the metadata value so the operator owns the wording.
|
|
174
334
|
const claimSource = str('claimSource');
|
|
175
335
|
if (claimSource !== '') {
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
336
|
+
const name = '创作来源';
|
|
337
|
+
const claimMatched = async (): Promise<boolean | null> => {
|
|
338
|
+
try {
|
|
339
|
+
const raw = (await page.evaluate(CLAIM_COMMITTED)) as { enabled: boolean | null; text: string } | null;
|
|
340
|
+
if (raw === null || typeof raw !== 'object') return null;
|
|
341
|
+
return raw.enabled === true && norm(raw.text) === norm(claimSource);
|
|
342
|
+
} catch {
|
|
343
|
+
return null;
|
|
182
344
|
}
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
|
|
345
|
+
};
|
|
346
|
+
if ((await claimMatched()) !== true) {
|
|
347
|
+
await step(name, async () => {
|
|
348
|
+
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);
|
|
353
|
+
}
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
if ((await claimMatched()) !== true && !(await residual(name))) {
|
|
357
|
+
warnings.push(`${name}: 未生效,请手动设置`);
|
|
358
|
+
}
|
|
186
359
|
}
|
|
187
360
|
|
|
188
361
|
await sweep();
|