@gobing-ai/knowledge-kit 0.1.1 → 0.1.2
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/package.json +1 -1
- package/plugins/generations/news-report-gen/dist/index.js +45 -1
- package/plugins/generations/news-report-gen/src/index.ts +78 -1
- package/plugins/kk/plugin.json +1 -1
- package/plugins/kk/scripts/kk-workflow-stages.ts +8 -0
- package/plugins/publishings/wechatmp-pub/dist/profile-check.js +6 -1
- package/plugins/publishings/wechatmp-pub/src/profile-check.ts +15 -1
- package/plugins/publishings/xhs-pub/dist/index.js +14 -1
- package/plugins/publishings/xhs-pub/dist/profile-check.js +14 -1
- package/plugins/publishings/xhs-pub/src/page.ts +25 -1
package/package.json
CHANGED
|
@@ -12395,6 +12395,27 @@ function minimumsFromEnv() {
|
|
|
12395
12395
|
}
|
|
12396
12396
|
return out;
|
|
12397
12397
|
}
|
|
12398
|
+
async function readPublishCompleteness(path) {
|
|
12399
|
+
if (!path || path.trim() === "")
|
|
12400
|
+
return null;
|
|
12401
|
+
try {
|
|
12402
|
+
const parsed = JSON.parse(await Bun.file(path).text());
|
|
12403
|
+
const rows = parsed?.rows;
|
|
12404
|
+
if (!Array.isArray(rows) || rows.length === 0)
|
|
12405
|
+
return null;
|
|
12406
|
+
const missing = parsed.missing;
|
|
12407
|
+
return {
|
|
12408
|
+
rows: rows.filter((row) => row !== null && typeof row === "object").map((row) => ({
|
|
12409
|
+
channel: typeof row.channel === "string" ? row.channel : "?",
|
|
12410
|
+
state: typeof row.state === "string" ? row.state : "?",
|
|
12411
|
+
detail: typeof row.detail === "string" ? row.detail : ""
|
|
12412
|
+
})),
|
|
12413
|
+
missing: Array.isArray(missing) ? missing.filter((name) => typeof name === "string") : []
|
|
12414
|
+
};
|
|
12415
|
+
} catch {
|
|
12416
|
+
return null;
|
|
12417
|
+
}
|
|
12418
|
+
}
|
|
12398
12419
|
async function readQcSummary(path) {
|
|
12399
12420
|
if (!path || path.trim() === "")
|
|
12400
12421
|
return null;
|
|
@@ -12561,6 +12582,19 @@ function renderReportMarkdown(docs, date, options = {}) {
|
|
|
12561
12582
|
lines.push(`| ${issue.type} | ${issue.count} |`);
|
|
12562
12583
|
lines.push("");
|
|
12563
12584
|
}
|
|
12585
|
+
const completeness = options.completeness ?? null;
|
|
12586
|
+
if (completeness !== null) {
|
|
12587
|
+
lines.push("## \u53D1\u5E03\u5B8C\u6574\u6027", "");
|
|
12588
|
+
lines.push("| \u901A\u9053 | \u72B6\u6001 | \u8BF4\u660E |", "| --- | --- | --- |");
|
|
12589
|
+
for (const row of completeness.rows) {
|
|
12590
|
+
const detail = row.detail.replace(/\|/g, "\\|").replace(/\s+/g, " ").slice(0, 200);
|
|
12591
|
+
lines.push(`| ${row.channel} | ${row.state} | ${detail || "\u2014"} |`);
|
|
12592
|
+
}
|
|
12593
|
+
lines.push("");
|
|
12594
|
+
if (completeness.missing.length > 0) {
|
|
12595
|
+
lines.push(`**\u672A\u5B8C\u6210\u901A\u9053**: ${completeness.missing.join(", ")} \u2014 \u8BE6\u7EC6\u539F\u56E0\u89C1\u4E0A\u8868\uFF0C\u91CD\u8DD1\u5BF9\u5E94 stage \u53EF\u8865\u53D1\uFF08'kk stage daily-publish-<channel> \u2026'\uFF09\u3002`, "");
|
|
12596
|
+
}
|
|
12597
|
+
}
|
|
12564
12598
|
const timing = options.timing ?? [];
|
|
12565
12599
|
if (timing.length > 0) {
|
|
12566
12600
|
lines.push("## \u6B65\u8017\u65F6", "");
|
|
@@ -12653,7 +12687,16 @@ async function processGeneratorIO(inputPath, outputPath) {
|
|
|
12653
12687
|
const scriptSource = await readScriptSource(process.env.NEWS_REPORT_SCRIPT_FILE);
|
|
12654
12688
|
const planAudit = await readPlanAudit(process.env.NEWS_REPORT_PLAN_AUDIT_FILE);
|
|
12655
12689
|
const blended = await readBlendedCorpus(process.env.NEWS_REPORT_BLENDED_FILE);
|
|
12656
|
-
const
|
|
12690
|
+
const completeness = await readPublishCompleteness(process.env.NEWS_REPORT_COMPLETENESS_FILE);
|
|
12691
|
+
const content = docsToContent(docs, dateFromEnv(), {
|
|
12692
|
+
rejected,
|
|
12693
|
+
timing,
|
|
12694
|
+
qc,
|
|
12695
|
+
scriptSource,
|
|
12696
|
+
planAudit,
|
|
12697
|
+
blended,
|
|
12698
|
+
completeness
|
|
12699
|
+
});
|
|
12657
12700
|
const fs = createNodeFileSystem();
|
|
12658
12701
|
const outDir = dirname2(outputPath);
|
|
12659
12702
|
if (outDir && outDir !== ".")
|
|
@@ -12692,6 +12735,7 @@ export {
|
|
|
12692
12735
|
processGeneratorIO,
|
|
12693
12736
|
readBlendedCorpus,
|
|
12694
12737
|
readPlanAudit,
|
|
12738
|
+
readPublishCompleteness,
|
|
12695
12739
|
readQcSummary,
|
|
12696
12740
|
readRejectedAudit,
|
|
12697
12741
|
readScriptSource,
|
|
@@ -27,6 +27,9 @@ import { echoError } from '@gobing-ai/ts-utils';
|
|
|
27
27
|
* - `NEWS_REPORT_SCRIPT_FILE` — the script stage's Content path
|
|
28
28
|
* (`24-script/<date>_08_script_content.json`, F613 R5); absent or metadata-less -> the
|
|
29
29
|
* 脚本来源 line is omitted, never throw.
|
|
30
|
+
* - `NEWS_REPORT_COMPLETENESS_FILE` — the publish tail's channel ledger
|
|
31
|
+
* (`31-publish/<date>_17_publish_completeness.json`, written by
|
|
32
|
+
* `kk stage daily-publish-completeness`); absent -> the 发布完整性 section is omitted, never throw.
|
|
30
33
|
* - `NEWS_REPORT_TIMING_FILE` — `<work_dir>/step-timing.json` JSONL of
|
|
31
34
|
* {step, startedAt, endedAt} appended by the workflow shell steps; missing
|
|
32
35
|
* or unreadable -> the timing section is omitted, never throw.
|
|
@@ -152,6 +155,49 @@ interface RejectedAudit {
|
|
|
152
155
|
reasons: string[];
|
|
153
156
|
}
|
|
154
157
|
|
|
158
|
+
/** One independent publish channel's terminal state, as the publish tail recorded it. */
|
|
159
|
+
export interface PublishCompletenessRow {
|
|
160
|
+
channel: string;
|
|
161
|
+
state: string;
|
|
162
|
+
detail: string;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export interface PublishCompleteness {
|
|
166
|
+
rows: PublishCompletenessRow[];
|
|
167
|
+
/** Channels that did not reach `done` — the ones the operator has to act on. */
|
|
168
|
+
missing: string[];
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Read the publish tail's completeness ledger (`kk stage daily-publish-completeness`).
|
|
173
|
+
*
|
|
174
|
+
* Best-effort by contract like every other report input: absent, unreadable, malformed, or
|
|
175
|
+
* row-less -> `null` and the section is omitted. The ledger is the only place the independent
|
|
176
|
+
* channels (xhs, wechat, locales, cms-resync) surface — they never enter `publish_status` — so
|
|
177
|
+
* 20261016 a run that drafted nothing to WeChat/XHS reported only its scoring and QC.
|
|
178
|
+
*/
|
|
179
|
+
export async function readPublishCompleteness(path: string | undefined): Promise<PublishCompleteness | null> {
|
|
180
|
+
if (!path || path.trim() === '') return null;
|
|
181
|
+
try {
|
|
182
|
+
const parsed: unknown = JSON.parse(await Bun.file(path).text());
|
|
183
|
+
const rows = (parsed as { rows?: unknown } | null)?.rows;
|
|
184
|
+
if (!Array.isArray(rows) || rows.length === 0) return null;
|
|
185
|
+
const missing = (parsed as { missing?: unknown }).missing;
|
|
186
|
+
return {
|
|
187
|
+
rows: rows
|
|
188
|
+
.filter((row): row is Record<string, unknown> => row !== null && typeof row === 'object')
|
|
189
|
+
.map((row) => ({
|
|
190
|
+
channel: typeof row.channel === 'string' ? row.channel : '?',
|
|
191
|
+
state: typeof row.state === 'string' ? row.state : '?',
|
|
192
|
+
detail: typeof row.detail === 'string' ? row.detail : '',
|
|
193
|
+
})),
|
|
194
|
+
missing: Array.isArray(missing) ? missing.filter((name): name is string => typeof name === 'string') : [],
|
|
195
|
+
};
|
|
196
|
+
} catch {
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
155
201
|
/** Generate-stage audio QC summary rendered into the run report (task 0139 R5b). */
|
|
156
202
|
export interface QcSummary {
|
|
157
203
|
passed: boolean;
|
|
@@ -278,6 +324,7 @@ export function renderReportMarkdown(
|
|
|
278
324
|
scriptSource?: ScriptSource | null;
|
|
279
325
|
planAudit?: PlanAuditRow[] | null;
|
|
280
326
|
blended?: BlendedDoc[] | null;
|
|
327
|
+
completeness?: PublishCompleteness | null;
|
|
281
328
|
} = {},
|
|
282
329
|
): string {
|
|
283
330
|
const lines: string[] = [`# 每日 AI 语音运行报告 — ${date}`, ''];
|
|
@@ -376,6 +423,26 @@ export function renderReportMarkdown(
|
|
|
376
423
|
lines.push('');
|
|
377
424
|
}
|
|
378
425
|
|
|
426
|
+
const completeness = options.completeness ?? null;
|
|
427
|
+
if (completeness !== null) {
|
|
428
|
+
// 20261016: the independent channels (xhs, wechat, locales, cms-resync) are non-blocking and
|
|
429
|
+
// never enter `publish_status`, so a run that drafted nothing to WeChat/XHS still reported
|
|
430
|
+
// `full`. Surface each channel's state here instead of leaving it to a separate JSON.
|
|
431
|
+
lines.push('## 发布完整性', '');
|
|
432
|
+
lines.push('| 通道 | 状态 | 说明 |', '| --- | --- | --- |');
|
|
433
|
+
for (const row of completeness.rows) {
|
|
434
|
+
const detail = row.detail.replace(/\|/g, '\\|').replace(/\s+/g, ' ').slice(0, 200);
|
|
435
|
+
lines.push(`| ${row.channel} | ${row.state} | ${detail || '—'} |`);
|
|
436
|
+
}
|
|
437
|
+
lines.push('');
|
|
438
|
+
if (completeness.missing.length > 0) {
|
|
439
|
+
lines.push(
|
|
440
|
+
`**未完成通道**: ${completeness.missing.join(', ')} — 详细原因见上表,重跑对应 stage 可补发('kk stage daily-publish-<channel> …')。`,
|
|
441
|
+
'',
|
|
442
|
+
);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
379
446
|
const timing = options.timing ?? [];
|
|
380
447
|
if (timing.length > 0) {
|
|
381
448
|
lines.push('## 步耗时', '');
|
|
@@ -498,6 +565,7 @@ export function docsToContent(
|
|
|
498
565
|
scriptSource?: ScriptSource | null;
|
|
499
566
|
planAudit?: PlanAuditRow[] | null;
|
|
500
567
|
blended?: BlendedDoc[] | null;
|
|
568
|
+
completeness?: PublishCompleteness | null;
|
|
501
569
|
},
|
|
502
570
|
): Content {
|
|
503
571
|
return ContentSchema.parse({
|
|
@@ -522,7 +590,16 @@ export async function processGeneratorIO(inputPath: string, outputPath: string):
|
|
|
522
590
|
const scriptSource = await readScriptSource(process.env.NEWS_REPORT_SCRIPT_FILE);
|
|
523
591
|
const planAudit = await readPlanAudit(process.env.NEWS_REPORT_PLAN_AUDIT_FILE);
|
|
524
592
|
const blended = await readBlendedCorpus(process.env.NEWS_REPORT_BLENDED_FILE);
|
|
525
|
-
const
|
|
593
|
+
const completeness = await readPublishCompleteness(process.env.NEWS_REPORT_COMPLETENESS_FILE);
|
|
594
|
+
const content = docsToContent(docs, dateFromEnv(), {
|
|
595
|
+
rejected,
|
|
596
|
+
timing,
|
|
597
|
+
qc,
|
|
598
|
+
scriptSource,
|
|
599
|
+
planAudit,
|
|
600
|
+
blended,
|
|
601
|
+
completeness,
|
|
602
|
+
});
|
|
526
603
|
const fs = createNodeFileSystem();
|
|
527
604
|
const outDir = dirname(outputPath);
|
|
528
605
|
if (outDir && outDir !== '.') await fs.ensureDir(outDir);
|
package/plugins/kk/plugin.json
CHANGED
|
@@ -1509,6 +1509,14 @@ function dailyRunReport(args: string[]): void {
|
|
|
1509
1509
|
NEWS_REPORT_BLENDED_FILE: join(absWorkDir, '10-ingest', `${runDate}_02_collect_blended.json`),
|
|
1510
1510
|
// F613 R5: the script stage's Content, read best-effort (absent -> 脚本来源 omitted).
|
|
1511
1511
|
NEWS_REPORT_SCRIPT_FILE: join(absWorkDir, '24-script', `${runDate}_08_script_content.json`),
|
|
1512
|
+
// 20261016: the independent publish channels (xhs/wechat/locales/cms-resync) never
|
|
1513
|
+
// enter publish_status, so the report is where an empty draft box becomes visible.
|
|
1514
|
+
// Written by the `daily-publish-completeness` stage that precedes this one.
|
|
1515
|
+
NEWS_REPORT_COMPLETENESS_FILE: join(
|
|
1516
|
+
absWorkDir,
|
|
1517
|
+
'31-publish',
|
|
1518
|
+
`${runDate}_17_publish_completeness.json`,
|
|
1519
|
+
),
|
|
1512
1520
|
NEWS_REPORT_DATE: runDate,
|
|
1513
1521
|
},
|
|
1514
1522
|
});
|
|
@@ -20828,6 +20828,8 @@ if (false) {}
|
|
|
20828
20828
|
// ../../plugins/publishings/wechatmp-pub/src/profile-check.ts
|
|
20829
20829
|
var USAGE = "usage: wechatmp profile-check [login|check|open] [--profile <dir>] [--url <url>] [--timeout <ms>] [--headed]";
|
|
20830
20830
|
var LOGIN_TIMEOUT_MS = 180000;
|
|
20831
|
+
var PROFILE_OVERHEAD_MS = 8000;
|
|
20832
|
+
var MIN_READINESS_MS = 1000;
|
|
20831
20833
|
async function main2(argv = process.argv.slice(2)) {
|
|
20832
20834
|
let command;
|
|
20833
20835
|
let profileDir;
|
|
@@ -20898,7 +20900,10 @@ async function main2(argv = process.argv.slice(2)) {
|
|
|
20898
20900
|
}
|
|
20899
20901
|
let verdict;
|
|
20900
20902
|
try {
|
|
20901
|
-
await checkWechatSession(profileDir, {
|
|
20903
|
+
await checkWechatSession(profileDir, {
|
|
20904
|
+
headless: !headed,
|
|
20905
|
+
timeoutMs: timeoutMs === undefined ? undefined : Math.max(MIN_READINESS_MS, timeoutMs - PROFILE_OVERHEAD_MS)
|
|
20906
|
+
});
|
|
20902
20907
|
verdict = {
|
|
20903
20908
|
status: "verified",
|
|
20904
20909
|
profileDir,
|
|
@@ -19,6 +19,16 @@ import { CHANNEL, CONSOLE_URL, checkWechatSession, openWechatLogin, openWechatSe
|
|
|
19
19
|
const USAGE =
|
|
20
20
|
'usage: wechatmp profile-check [login|check|open] [--profile <dir>] [--url <url>] [--timeout <ms>] [--headed]';
|
|
21
21
|
const LOGIN_TIMEOUT_MS = 180_000;
|
|
22
|
+
/**
|
|
23
|
+
* The one browser open, console navigation and context close a `check` performs around its readiness
|
|
24
|
+
* poll (headless ~3-4s, headed ~5-8s). `--timeout` is the WHOLE probe budget — kk SIGKILLs the process
|
|
25
|
+
* at that instant — so the poll must not spend all of it: a dead session then dies mid-report and
|
|
26
|
+
* surfaces as `probe exit 1 (malformed probe output)` instead of `refused` (observed 2026-10-16, where
|
|
27
|
+
* the 30s poll plus overhead overran `kk plugin check`'s 30s default).
|
|
28
|
+
*/
|
|
29
|
+
const PROFILE_OVERHEAD_MS = 8_000;
|
|
30
|
+
/** Floor for the readiness poll, so a tiny `--timeout` still probes the console instead of refusing blind. */
|
|
31
|
+
const MIN_READINESS_MS = 1_000;
|
|
22
32
|
|
|
23
33
|
interface WechatVerdict {
|
|
24
34
|
status: 'verified' | 'refused';
|
|
@@ -106,7 +116,11 @@ export async function main(argv: string[] = process.argv.slice(2)): Promise<numb
|
|
|
106
116
|
|
|
107
117
|
let verdict: WechatVerdict;
|
|
108
118
|
try {
|
|
109
|
-
await checkWechatSession(profileDir, {
|
|
119
|
+
await checkWechatSession(profileDir, {
|
|
120
|
+
headless: !headed,
|
|
121
|
+
timeoutMs:
|
|
122
|
+
timeoutMs === undefined ? undefined : Math.max(MIN_READINESS_MS, timeoutMs - PROFILE_OVERHEAD_MS),
|
|
123
|
+
});
|
|
110
124
|
verdict = {
|
|
111
125
|
status: 'verified',
|
|
112
126
|
profileDir,
|
|
@@ -21269,6 +21269,10 @@ async function validateModeInput(operation, content) {
|
|
|
21269
21269
|
}
|
|
21270
21270
|
|
|
21271
21271
|
// ../../plugins/publishings/xhs-pub/src/page.ts
|
|
21272
|
+
var TRANSIENT_NAVIGATION = /net::ERR_(?:TIMED_OUT|CONNECTION_(?:RESET|CLOSED|TIMED_OUT|ABORTED|REFUSED|FAILED)|NETWORK_(?:CHANGED|IO_SUSPENDED)|EMPTY_RESPONSE|ADDRESS_UNREACHABLE|NAME_NOT_RESOLVED)/;
|
|
21273
|
+
function isTransientNavigationError(error) {
|
|
21274
|
+
return TRANSIENT_NAVIGATION.test(error instanceof Error ? error.message : String(error));
|
|
21275
|
+
}
|
|
21272
21276
|
var POLL_MS = 500;
|
|
21273
21277
|
var STEP_TIMEOUT_MS = 30000;
|
|
21274
21278
|
var CREATOR_TAB_BAR = "div.creator-tab";
|
|
@@ -21314,7 +21318,16 @@ function wrapPlaywrightPage(raw, shadow) {
|
|
|
21314
21318
|
return found;
|
|
21315
21319
|
};
|
|
21316
21320
|
return {
|
|
21317
|
-
goto: (url, options) =>
|
|
21321
|
+
goto: async (url, options) => {
|
|
21322
|
+
const navigate = () => pw.goto(url, { waitUntil: "domcontentloaded", ...options });
|
|
21323
|
+
try {
|
|
21324
|
+
await navigate();
|
|
21325
|
+
} catch (error) {
|
|
21326
|
+
if (!isTransientNavigationError(error))
|
|
21327
|
+
throw error;
|
|
21328
|
+
await navigate();
|
|
21329
|
+
}
|
|
21330
|
+
},
|
|
21318
21331
|
locator: (selector) => ({
|
|
21319
21332
|
count: () => pw.locator(selector).count(),
|
|
21320
21333
|
first: () => {
|
|
@@ -20947,6 +20947,10 @@ if (false) {}
|
|
|
20947
20947
|
var PUBLISH_URL = "https://creator.xiaohongshu.com/publish/publish";
|
|
20948
20948
|
|
|
20949
20949
|
// ../../plugins/publishings/xhs-pub/src/page.ts
|
|
20950
|
+
var TRANSIENT_NAVIGATION = /net::ERR_(?:TIMED_OUT|CONNECTION_(?:RESET|CLOSED|TIMED_OUT|ABORTED|REFUSED|FAILED)|NETWORK_(?:CHANGED|IO_SUSPENDED)|EMPTY_RESPONSE|ADDRESS_UNREACHABLE|NAME_NOT_RESOLVED)/;
|
|
20951
|
+
function isTransientNavigationError(error) {
|
|
20952
|
+
return TRANSIENT_NAVIGATION.test(error instanceof Error ? error.message : String(error));
|
|
20953
|
+
}
|
|
20950
20954
|
function wrapPlaywrightPage(raw, shadow) {
|
|
20951
20955
|
const pw = raw;
|
|
20952
20956
|
const onScreen = async (selector) => {
|
|
@@ -20968,7 +20972,16 @@ function wrapPlaywrightPage(raw, shadow) {
|
|
|
20968
20972
|
return found;
|
|
20969
20973
|
};
|
|
20970
20974
|
return {
|
|
20971
|
-
goto: (url, options) =>
|
|
20975
|
+
goto: async (url, options) => {
|
|
20976
|
+
const navigate = () => pw.goto(url, { waitUntil: "domcontentloaded", ...options });
|
|
20977
|
+
try {
|
|
20978
|
+
await navigate();
|
|
20979
|
+
} catch (error) {
|
|
20980
|
+
if (!isTransientNavigationError(error))
|
|
20981
|
+
throw error;
|
|
20982
|
+
await navigate();
|
|
20983
|
+
}
|
|
20984
|
+
},
|
|
20972
20985
|
locator: (selector) => ({
|
|
20973
20986
|
count: () => pw.locator(selector).count(),
|
|
20974
20987
|
first: () => {
|
|
@@ -10,6 +10,19 @@ import { waitUntil } from '@gobing-ai/publish-harness/browser';
|
|
|
10
10
|
* Playwright page is part of this seam, never reached around it.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
+
/**
|
|
14
|
+
* Connect faults this origin produces intermittently (2026-10-16: `net::ERR_TIMED_OUT` at the
|
|
15
|
+
* publish page; 2026-10-13: the preflight's `Timeout 30000ms exceeded — waiting until "load"`).
|
|
16
|
+
* One retry absorbs the flake; a second failure is reported as it is, so a real outage still fails.
|
|
17
|
+
*/
|
|
18
|
+
const TRANSIENT_NAVIGATION =
|
|
19
|
+
/net::ERR_(?:TIMED_OUT|CONNECTION_(?:RESET|CLOSED|TIMED_OUT|ABORTED|REFUSED|FAILED)|NETWORK_(?:CHANGED|IO_SUSPENDED)|EMPTY_RESPONSE|ADDRESS_UNREACHABLE|NAME_NOT_RESOLVED)/;
|
|
20
|
+
|
|
21
|
+
/** Whether a navigation failed for a reason a single retry can plausibly clear. */
|
|
22
|
+
function isTransientNavigationError(error: unknown): boolean {
|
|
23
|
+
return TRANSIENT_NAVIGATION.test(error instanceof Error ? error.message : String(error));
|
|
24
|
+
}
|
|
25
|
+
|
|
13
26
|
export const POLL_MS = 500;
|
|
14
27
|
|
|
15
28
|
// The shared poll (0184 R2); re-exported so flow/index keep importing it from here.
|
|
@@ -169,7 +182,18 @@ export function wrapPlaywrightPage(raw: unknown, shadow?: ShadowProbe): XhsPage
|
|
|
169
182
|
};
|
|
170
183
|
|
|
171
184
|
return {
|
|
172
|
-
|
|
185
|
+
// `domcontentloaded`, never Playwright's `load` default: the console keeps long-lived
|
|
186
|
+
// subresources open, so a `load` wait turns a healthy visit into a timeout (2026-10-13
|
|
187
|
+
// preflight). Pinned here once, the way wechatmp-pub/src/page.ts already pins it.
|
|
188
|
+
goto: async (url, options) => {
|
|
189
|
+
const navigate = (): Promise<unknown> => pw.goto(url, { waitUntil: 'domcontentloaded', ...options });
|
|
190
|
+
try {
|
|
191
|
+
await navigate();
|
|
192
|
+
} catch (error) {
|
|
193
|
+
if (!isTransientNavigationError(error)) throw error;
|
|
194
|
+
await navigate();
|
|
195
|
+
}
|
|
196
|
+
},
|
|
173
197
|
locator: (selector) => ({
|
|
174
198
|
count: () => pw.locator(selector).count(),
|
|
175
199
|
first: () => {
|