@gobing-ai/knowledge-kit 0.0.32 → 0.0.33
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/index.js +32320 -15021
- package/package.json +4 -4
- package/plugins/generations/content-gen/dist/index.js +10 -9
- package/plugins/generations/core-facts-gen/dist/index.js +13 -12
- package/plugins/generations/daily-article-gen/dist/index.js +14 -13
- package/plugins/generations/daily-article-gen/src/index.ts +1 -1
- package/plugins/generations/dailynews-gen/dist/index.js +10 -9
- package/plugins/generations/episode-plan-gen/dist/index.js +58 -15
- package/plugins/generations/episode-plan-gen/src/index.ts +76 -6
- package/plugins/generations/gemini-tts/README.md +11 -0
- package/plugins/generations/gemini-tts/dist/index.js +10 -9
- package/plugins/generations/image-gen/dist/index.js +154 -151
- package/plugins/generations/image-gen/src/compose/chromium.ts +60 -42
- package/plugins/generations/news-report-gen/dist/index.js +13 -12
- package/plugins/generations/voice-gen/dist/index.js +10 -9
- package/plugins/ingestions/aihot-ingest/dist/index.js +13 -12
- package/plugins/ingestions/horizon-ingest/dist/index.js +13 -12
- package/plugins/ingestions/karakeep-local/dist/index.js +13 -13
- package/plugins/ingestions/last30days-ingest/dist/index.js +13 -12
- package/plugins/ingestions/web-search/dist/index.js +11 -10
- package/plugins/kk/README.md +38 -0
- package/plugins/kk/decisions/extract/ai-news.yaml +21 -0
- package/plugins/kk/decisions/kk-items.yaml +113 -0
- package/plugins/kk/plugin.json +1 -1
- package/plugins/kk/schedules/cron.tmpl +15 -0
- package/plugins/kk/schedules/launchd.plist.tmpl +77 -0
- package/plugins/kk/scripts/daily-stages.ts +1533 -0
- package/plugins/kk/scripts/kk-workflow-stages.ts +247 -1365
- package/plugins/kk/skills/content-safety/SKILL.md +38 -7
- package/plugins/kk/skills/web-automation/SKILL.md +100 -0
- package/plugins/kk/skills/web-automation/references/browser-launch.md +86 -0
- package/plugins/kk/workflows/kk-collect.yaml +258 -0
- package/plugins/kk/workflows/kk-daily-ai-voice.yaml +9 -3
- package/plugins/kk/workflows/kk-daily-ai-voice2.yaml +655 -0
- package/plugins/kk/workflows/kk-daily-safety.yaml +170 -0
- package/plugins/publishings/emdash-pub/dist/index.js +10 -9
- package/plugins/publishings/infoq-pub/dist/index.js +152 -59
- package/plugins/publishings/infoq-pub/plugin.json +1 -0
- package/plugins/publishings/infoq-pub/src/index.ts +167 -55
- package/plugins/publishings/infoq-pub/src/profile-check.ts +4 -2
- package/plugins/publishings/juejin-pub/dist/index.js +111 -30
- package/plugins/publishings/juejin-pub/plugin.json +1 -0
- package/plugins/publishings/juejin-pub/src/index.ts +49 -9
- package/plugins/publishings/medium-pub/dist/index.js +10 -9
- package/plugins/publishings/podcast-pub/dist/index.js +10 -9
- package/plugins/publishings/qiita-pub/dist/index.js +10 -9
- package/plugins/publishings/substack-pub/dist/index.js +105 -28
- package/plugins/publishings/substack-pub/plugin.json +1 -0
- package/plugins/publishings/substack-pub/src/index.ts +39 -8
- package/plugins/publishings/substack-pub/src/profile-check.ts +2 -0
- package/plugins/publishings/surfdash-pub/dist/index.js +10 -9
- package/plugins/publishings/wechatmp-pub/dist/index.js +115 -31
- package/plugins/publishings/wechatmp-pub/plugin.json +1 -0
- package/plugins/publishings/wechatmp-pub/src/index.ts +42 -9
- package/plugins/publishings/wechatmp-pub/src/profile-check.ts +84 -40
- package/plugins/publishings/x-pub/dist/index.js +114 -35
- package/plugins/publishings/x-pub/plugin.json +1 -0
- package/plugins/publishings/x-pub/src/index.ts +68 -14
- package/plugins/publishings/xhs-pub/dist/index.js +100 -54
- package/plugins/publishings/xhs-pub/plugin.json +1 -0
- package/plugins/publishings/xhs-pub/src/index.ts +8 -4
- package/plugins/publishings/xhs-pub/src/page.ts +4 -17
- package/plugins/publishings/xhs-pub/src/profile-check.ts +28 -15
- package/plugins/publishings/zenn-pub/dist/index.js +10 -9
|
@@ -11,7 +11,9 @@ import {
|
|
|
11
11
|
import {
|
|
12
12
|
type ChannelBrowserConfig,
|
|
13
13
|
checkChannelSession,
|
|
14
|
+
DEFAULT_SIGNAL_CONTAINERS,
|
|
14
15
|
expectOutcome,
|
|
16
|
+
kkBrowserLauncher,
|
|
15
17
|
markdownToHtml,
|
|
16
18
|
pasteHtml,
|
|
17
19
|
readScopedSignal,
|
|
@@ -33,48 +35,105 @@ import type { Page } from 'playwright';
|
|
|
33
35
|
export const CHANNEL = 'infoq';
|
|
34
36
|
const TARGET = 'infoq';
|
|
35
37
|
/**
|
|
36
|
-
* InfoQ
|
|
37
|
-
*
|
|
38
|
+
* The live entry to the InfoQ editor (measured 2026-10-06, tasks 0201 + 0191): the home page's
|
|
39
|
+
* 写点什么 write button SPA-routes to `/draft/<id>`, where the draft already exists server-side.
|
|
40
|
+
* The former `https://xie.infoq.cn/write` constant is dead — it renders an empty shell even
|
|
41
|
+
* authenticated, with no editor and no login redirect.
|
|
38
42
|
*/
|
|
39
|
-
export const
|
|
40
|
-
/**
|
|
41
|
-
export const
|
|
43
|
+
export const HOME_URL = 'https://xie.infoq.cn/';
|
|
44
|
+
/** The editor URL the entry flow lands on; the captured group is the server-side draft id. */
|
|
45
|
+
export const DRAFT_URL_RE = /\/draft\/(\d+)/;
|
|
46
|
+
/**
|
|
47
|
+
* Login/SSO URLs that mean "not authenticated" — the editor never renders behind them. The SSO
|
|
48
|
+
* domain moved from account.infoq.cn to account.geekbang.org (measured, task 0201). The home page
|
|
49
|
+
* renders logged-out content without redirecting, so this regex is only the fast arm of the
|
|
50
|
+
* readiness predicate below.
|
|
51
|
+
*/
|
|
52
|
+
export const LOGIN_URL_RE = /(account\.infoq\.cn|account\.geekbang\.org|\/login)/;
|
|
42
53
|
|
|
43
54
|
/**
|
|
44
|
-
* Selectors measured against the
|
|
45
|
-
* tried in order
|
|
46
|
-
*
|
|
55
|
+
* Selectors measured against the live authenticated console (2026-10-06, tasks 0201 + 0191);
|
|
56
|
+
* each list is tried in order. Measured absent and therefore removed: the summary field (the
|
|
57
|
+
* console has no 摘要 input, so `metadata.summary` is not representable on InfoQ) and the draft
|
|
58
|
+
* button (the editor autosaves — the save signal is the 已保存 indicator, not a click). `live`
|
|
59
|
+
* is the 发布 control, a `div`: InfoQ submit goes to editorial review, the site has no
|
|
60
|
+
* self-publish control.
|
|
47
61
|
*/
|
|
48
62
|
export const SELECTORS = {
|
|
49
|
-
|
|
50
|
-
|
|
63
|
+
writeEntry: ['div.write-btn'],
|
|
64
|
+
noticeDismiss: ['text=知道了'],
|
|
65
|
+
title: ['input[placeholder="请输入标题"]', 'input[placeholder*="标题"]'],
|
|
51
66
|
body: ['.ProseMirror[contenteditable="true"]', '[contenteditable="true"]'],
|
|
52
|
-
|
|
53
|
-
live: ['button:has-text("提交")'],
|
|
67
|
+
live: ['div.submit-btn'],
|
|
54
68
|
} as const;
|
|
55
69
|
|
|
70
|
+
/**
|
|
71
|
+
* Per-step wait. Lower bound: the measured autosave indicator appears within ~6 s of content
|
|
72
|
+
* entry (docs/design/infoq-ui-discovery.md:41, 0201 walk), so the window must cover the slow end
|
|
73
|
+
* of that latency plus hydration; 30 s is the 5× margin. Unit tests override this for fakes that
|
|
74
|
+
* resolve instantly.
|
|
75
|
+
*/
|
|
56
76
|
const STEP_TIMEOUT_MS = 30_000;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Read-only session check budget, independent of {@link STEP_TIMEOUT_MS}: headed hydration of
|
|
80
|
+
* the logged-in home has a slow tail (2.5-35 s measured on the operator profile, 0191 dogfood),
|
|
81
|
+
* so one 30 s step window is not enough for the check.
|
|
82
|
+
*/
|
|
83
|
+
const CHECK_TIMEOUT_MS = 60_000;
|
|
57
84
|
const LOGIN_TIMEOUT_MS = 180_000;
|
|
58
85
|
/** Operator recovery command every session failure names — never a cookie, page HTML or stack. */
|
|
59
86
|
const LOGIN_INSTRUCTION = 'bun plugins/publishings/infoq-pub/src/profile-check.ts login';
|
|
60
|
-
/**
|
|
61
|
-
|
|
62
|
-
|
|
87
|
+
/**
|
|
88
|
+
* The draft-saved signal (measured, task 0201; re-walked 0191): the editor autosaves and shows
|
|
89
|
+
* the 已保存 indicator — read only from the page's notification containers, never the article
|
|
90
|
+
* body (residual 0195).
|
|
91
|
+
*/
|
|
92
|
+
const DRAFT_SAVED_TEXTS = ['已保存'];
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Signal containers for this channel: the shared toast/notice defaults plus the editor header's
|
|
96
|
+
* persistent save indicator (`div.save-status`, measured 0191 re-dogfood — InfoQ now renders
|
|
97
|
+
* `已保存 <timestamp>` there instead of a toast). Class-scoped, so the 0195 body false-positive
|
|
98
|
+
* risk does not apply.
|
|
99
|
+
*/
|
|
100
|
+
const SIGNAL_CONTAINERS: readonly string[] = [...DEFAULT_SIGNAL_CONTAINERS, '[class*="save-status" i]'];
|
|
63
101
|
const LIVE_TOASTS = ['提交成功', '审核'];
|
|
102
|
+
/**
|
|
103
|
+
* Page-side scan behind {@link isAuthenticated}: is a control labelled exactly 登录 visible?
|
|
104
|
+
* Exact leaf text, because feed articles can carry the substring 登录 without the session being
|
|
105
|
+
* logged out.
|
|
106
|
+
*/
|
|
107
|
+
const LOGGED_OUT_MARKER_SCRIPT = `(() => {
|
|
108
|
+
for (const node of document.querySelectorAll('body *')) {
|
|
109
|
+
if (node.children.length > 0) continue;
|
|
110
|
+
if ((node.textContent ?? '').trim() !== '登录') continue;
|
|
111
|
+
if (typeof node.checkVisibility === 'function' ? !node.checkVisibility() : node.getClientRects().length === 0) continue;
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
return false;
|
|
115
|
+
})()`;
|
|
64
116
|
|
|
65
117
|
/**
|
|
66
|
-
* InfoQ-owned readiness predicate: the
|
|
67
|
-
* is
|
|
118
|
+
* InfoQ-owned readiness predicate: the home page has hydrated to the write entry and no 登录
|
|
119
|
+
* control is visible (the logged-out marker). Shared by the publish preflight and the
|
|
120
|
+
* profile-session CLI (R1, R5). Hydration gates the marker read: until the home chrome renders,
|
|
121
|
+
* the 登录 control has not attached yet and a logged-out page false-reads logged-in (0201).
|
|
68
122
|
*/
|
|
69
123
|
export const isAuthenticated: IsAuthenticatedPredicate = async (page) =>
|
|
70
|
-
!LOGIN_URL_RE.test(page.url()) &&
|
|
124
|
+
!LOGIN_URL_RE.test(page.url()) &&
|
|
125
|
+
(await trySelectors(page, SELECTORS.writeEntry, { visible: false })).found &&
|
|
126
|
+
!(await page.evaluate(LOGGED_OUT_MARKER_SCRIPT));
|
|
71
127
|
|
|
72
128
|
/** Site-owned config for the shared session check and operator CLI (0183). */
|
|
73
129
|
export const CONFIG: ChannelBrowserConfig = {
|
|
74
130
|
channel: CHANNEL,
|
|
75
|
-
homeUrl:
|
|
131
|
+
homeUrl: HOME_URL,
|
|
76
132
|
isAuthenticated,
|
|
77
133
|
loginTimeoutMs: LOGIN_TIMEOUT_MS,
|
|
134
|
+
// The WAF 451s every headless load, so the profile-check CLI must not waste its first
|
|
135
|
+
// pass headlessly either (measured 0201, re-confirmed 0191).
|
|
136
|
+
checkHeadedOnly: true,
|
|
78
137
|
};
|
|
79
138
|
|
|
80
139
|
function fail(error: string): Result {
|
|
@@ -124,25 +183,54 @@ async function findSelector(
|
|
|
124
183
|
* a body containing `保存成功` reported a draft that was never saved.
|
|
125
184
|
*/
|
|
126
185
|
async function readToast(page: Page, texts: readonly string[]): Promise<string | null> {
|
|
127
|
-
return readScopedSignal(page, texts);
|
|
186
|
+
return readScopedSignal(page, texts, SIGNAL_CONTAINERS);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Declared modes (ADR-029 dispatch; task 0205 R2): InfoQ's only console flow is the draft path; 提交 (editorial review) is live-only and not a mode. An unknown operation is refused by
|
|
191
|
+
* name before a profile is resolved or a browser opens.
|
|
192
|
+
*/
|
|
193
|
+
export const INFOQ_OPERATIONS = ['draft'] as const;
|
|
194
|
+
export type InfoqOperation = (typeof INFOQ_OPERATIONS)[number];
|
|
195
|
+
|
|
196
|
+
/** Exit-early resolution of the requested operation; a default Content without one resolves to `draft`. */
|
|
197
|
+
export function resolveOperation(
|
|
198
|
+
options?: Record<string, unknown>,
|
|
199
|
+
content?: Content,
|
|
200
|
+
): { operation: InfoqOperation; error?: undefined } | { operation?: undefined; error: string } {
|
|
201
|
+
const raw = options?.operation ?? content?.metadata?.operation;
|
|
202
|
+
if (raw === undefined) return { operation: 'draft' };
|
|
203
|
+
if (typeof raw !== 'string' || !(INFOQ_OPERATIONS as readonly string[]).includes(raw)) {
|
|
204
|
+
return {
|
|
205
|
+
error: `unsupported operation '${String(raw)}' for infoq: expected one of ${INFOQ_OPERATIONS.join(', ')}`,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
return { operation: raw as InfoqOperation };
|
|
128
209
|
}
|
|
129
210
|
|
|
130
211
|
export class InfoqBrowserTransport implements PublishTransport {
|
|
131
212
|
public readonly kind = 'browser';
|
|
132
213
|
public readonly driver: BrowserDriver;
|
|
133
|
-
private readonly launcher
|
|
214
|
+
private readonly launcher: BrowserLauncher;
|
|
134
215
|
private readonly stepTimeoutMs: number;
|
|
216
|
+
private readonly checkTimeoutMs: number;
|
|
135
217
|
|
|
136
|
-
constructor(
|
|
218
|
+
constructor(
|
|
219
|
+
driver: BrowserDriver = 'script',
|
|
220
|
+
deps: { launcher?: BrowserLauncher; stepTimeoutMs?: number; checkTimeoutMs?: number } = {},
|
|
221
|
+
) {
|
|
137
222
|
this.driver = driver;
|
|
138
|
-
this.launcher = deps.launcher;
|
|
223
|
+
this.launcher = deps.launcher ?? kkBrowserLauncher;
|
|
139
224
|
this.stepTimeoutMs = deps.stepTimeoutMs ?? STEP_TIMEOUT_MS;
|
|
225
|
+
this.checkTimeoutMs = deps.checkTimeoutMs ?? CHECK_TIMEOUT_MS;
|
|
140
226
|
}
|
|
141
227
|
|
|
142
228
|
public async publish(payload: PublishTransportPayload): Promise<Result> {
|
|
143
229
|
if (this.driver !== 'script') {
|
|
144
230
|
return fail(`unsupported driver '${this.driver}' for ${TARGET}`);
|
|
145
231
|
}
|
|
232
|
+
const mode = resolveOperation(payload.options, payload.content);
|
|
233
|
+
if (mode.error !== undefined) return fail(mode.error);
|
|
146
234
|
const requested = payload.content.metadata?.publishStatus;
|
|
147
235
|
if (requested !== undefined && requested !== 'draft' && requested !== 'public') {
|
|
148
236
|
return fail(`unknown publishStatus '${String(requested)}' (expected absent, 'draft' or 'public')`);
|
|
@@ -151,13 +239,17 @@ export class InfoqBrowserTransport implements PublishTransport {
|
|
|
151
239
|
const override = typeof payload.options?.profileDir === 'string' ? payload.options.profileDir : undefined;
|
|
152
240
|
const profileDir = resolveChannelProfileDir(CHANNEL, override);
|
|
153
241
|
|
|
154
|
-
// ADR-025 preflight: an unauthenticated or absent profile must never
|
|
155
|
-
//
|
|
242
|
+
// ADR-025 preflight: an unauthenticated or absent profile must never reach the editor
|
|
243
|
+
// flow, so the read-only session check runs first and a refusal stays a fast refusal.
|
|
156
244
|
const session = await checkChannelSession(CONFIG, {
|
|
157
245
|
profileDir,
|
|
158
246
|
launcher: this.launcher,
|
|
159
|
-
|
|
160
|
-
|
|
247
|
+
// InfoQ's WAF serves HTTP 451 to every headless page load (measured 0201, re-confirmed
|
|
248
|
+
// 0191), so the check runs headed only — a headless pass is a guaranteed 30 s burn
|
|
249
|
+
// that shortens the headed budget below the measured hydration tail. A
|
|
250
|
+
// missing/expired profile still refuses before any flow opens (ADR-025 shape).
|
|
251
|
+
headedOnly: true,
|
|
252
|
+
timeoutMs: this.checkTimeoutMs,
|
|
161
253
|
});
|
|
162
254
|
if (session.status !== 'verified') {
|
|
163
255
|
return fail(`${CHANNEL} session preflight refused: ${session.detail} — run: ${LOGIN_INSTRUCTION}`);
|
|
@@ -167,10 +259,11 @@ export class InfoqBrowserTransport implements PublishTransport {
|
|
|
167
259
|
return await withProfilePage(
|
|
168
260
|
{
|
|
169
261
|
profileDir,
|
|
170
|
-
url:
|
|
262
|
+
url: HOME_URL,
|
|
171
263
|
isAuthenticated,
|
|
172
|
-
// Headed by default
|
|
173
|
-
//
|
|
264
|
+
// Headed by default — InfoQ's WAF 451s every headless load (measured 0201,
|
|
265
|
+
// re-confirmed 0191; see the preflight comment); `options.headless === true`
|
|
266
|
+
// is an explicit opt-in that will fail loudly against that wall.
|
|
174
267
|
headless: payload.options?.headless === true,
|
|
175
268
|
launcher: this.launcher,
|
|
176
269
|
// The session is already preflight-verified, so the post-goto confirmation is
|
|
@@ -186,9 +279,12 @@ export class InfoqBrowserTransport implements PublishTransport {
|
|
|
186
279
|
}
|
|
187
280
|
|
|
188
281
|
/**
|
|
189
|
-
* The site-owned editor flow
|
|
190
|
-
*
|
|
191
|
-
*
|
|
282
|
+
* The site-owned editor flow (measured 0201 + 0191): home 写点什么 → the server-side draft
|
|
283
|
+
* `/draft/<id>` → title → ProseMirror paste → autosave indicator. There is no draft click:
|
|
284
|
+
* the entry itself created the draft, and the save is proven by the observed 已保存 signal.
|
|
285
|
+
* The submit control is touched only for `publishStatus === 'public'`. Every outcome comes
|
|
286
|
+
* from an `expectOutcome` probe, so a step without an observed signal is never a success
|
|
287
|
+
* (E91-D6).
|
|
192
288
|
*/
|
|
193
289
|
private async runEditorFlow(
|
|
194
290
|
page: Page,
|
|
@@ -198,33 +294,46 @@ export class InfoqBrowserTransport implements PublishTransport {
|
|
|
198
294
|
): Promise<Result> {
|
|
199
295
|
const metadata = { driver: this.driver, publishStatus, profileDir };
|
|
200
296
|
|
|
297
|
+
const entrySelector = await findSelector(page, SELECTORS.writeEntry, 'write entry', this.stepTimeoutMs);
|
|
298
|
+
await page.locator(entrySelector).first().click();
|
|
299
|
+
const entered = await waitUntil(() => Promise.resolve(DRAFT_URL_RE.test(page.url())), this.stepTimeoutMs);
|
|
300
|
+
if (!entered) {
|
|
301
|
+
throw new Error(
|
|
302
|
+
`infoq editor entry: still on '${page.url()}' — no /draft/<id> within ${this.stepTimeoutMs}ms`,
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
// The draft id exists before any content is typed: the entry itself created the draft.
|
|
306
|
+
const id = DRAFT_URL_RE.exec(page.url())?.[1];
|
|
307
|
+
await dismissNoticeModal(page, this.stepTimeoutMs);
|
|
308
|
+
|
|
201
309
|
const titleSelector = await findSelector(page, SELECTORS.title, 'title input', this.stepTimeoutMs);
|
|
202
310
|
await page
|
|
203
311
|
.locator(titleSelector)
|
|
204
312
|
.first()
|
|
205
313
|
.fill(content.title ?? '');
|
|
206
314
|
|
|
207
|
-
|
|
208
|
-
if (typeof summary === 'string' && summary.length > 0) {
|
|
209
|
-
const summarySelector = await findSelector(page, SELECTORS.summary, 'summary input', this.stepTimeoutMs);
|
|
210
|
-
await page.locator(summarySelector).first().fill(summary);
|
|
211
|
-
}
|
|
315
|
+
// No summary step: the measured console has no summary field (see SELECTORS).
|
|
212
316
|
|
|
213
317
|
const bodySelector = await findSelector(page, SELECTORS.body, 'body editor', this.stepTimeoutMs);
|
|
214
318
|
await pasteHtml(page, bodySelector, markdownToHtml(content.body), { timeoutMs: this.stepTimeoutMs });
|
|
319
|
+
// pasteHtml sets innerHTML without an input event, so ProseMirror's autosave may not see
|
|
320
|
+
// the change; dispatch one so the 已保存 indicator actually fires (measured 0191 probe:
|
|
321
|
+
// the indicator appeared only after a real input event reached the editor).
|
|
322
|
+
// String script: this plugin's tsconfig has no DOM lib, so the callback must not be TS.
|
|
323
|
+
await page
|
|
324
|
+
.evaluate(
|
|
325
|
+
'(selector) => { const editor = document.querySelector(selector); editor && editor.dispatchEvent(new InputEvent("input", { bubbles: true })); }',
|
|
326
|
+
bodySelector,
|
|
327
|
+
)
|
|
328
|
+
.catch(() => {});
|
|
215
329
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
await
|
|
219
|
-
|
|
220
|
-
const draft = await expectOutcome(() => probeDraft(page, urlBeforeSave), {
|
|
330
|
+
// Signal = the autosave 已保存 indicator, measured within ~6 s of paste (0201 walk);
|
|
331
|
+
// the 30 s default window is the 5× margin (see STEP_TIMEOUT_MS).
|
|
332
|
+
const draft = await expectOutcome(() => readToast(page, DRAFT_SAVED_TEXTS), {
|
|
221
333
|
timeoutMs: this.stepTimeoutMs,
|
|
222
|
-
label:
|
|
334
|
+
label: 'infoq draft autosave',
|
|
223
335
|
});
|
|
224
336
|
if (!draft.ok) return fail(draft.error);
|
|
225
|
-
// The probe only returns a URL that moved and carried an id, so a capture exists exactly
|
|
226
|
-
// for that branch; a toast text carries none.
|
|
227
|
-
const id = DRAFT_URL_ID_RE.exec(draft.evidence)?.[1];
|
|
228
337
|
|
|
229
338
|
if (publishStatus === 'draft') {
|
|
230
339
|
return ResultSchema.parse({
|
|
@@ -238,8 +347,8 @@ export class InfoqBrowserTransport implements PublishTransport {
|
|
|
238
347
|
}
|
|
239
348
|
|
|
240
349
|
const liveSelector = await findSelector(page, SELECTORS.live, 'submit control', this.stepTimeoutMs);
|
|
241
|
-
// Captured at the click, not at the
|
|
242
|
-
//
|
|
350
|
+
// Captured at the click, not at the entry: the entry already moved the URL to
|
|
351
|
+
// /draft/<id>, and an entry-time baseline would read that move as the submit's outcome.
|
|
243
352
|
const urlBeforeSubmit = page.url();
|
|
244
353
|
await page.locator(liveSelector).first().click();
|
|
245
354
|
const live = await expectOutcome(() => probeLive(page, urlBeforeSubmit), {
|
|
@@ -260,13 +369,16 @@ export class InfoqBrowserTransport implements PublishTransport {
|
|
|
260
369
|
}
|
|
261
370
|
|
|
262
371
|
/**
|
|
263
|
-
*
|
|
264
|
-
*
|
|
372
|
+
* The feature-notice modal overlays the editor and swallows click/fill events (measured 0201);
|
|
373
|
+
* it is not always shown, so dismissal is a guarded no-op when absent.
|
|
265
374
|
*/
|
|
266
|
-
async function
|
|
267
|
-
const
|
|
268
|
-
|
|
269
|
-
|
|
375
|
+
async function dismissNoticeModal(page: Page, timeoutMs: number): Promise<void> {
|
|
376
|
+
const modal = page.locator(SELECTORS.noticeDismiss[0]).first();
|
|
377
|
+
try {
|
|
378
|
+
if ((await modal.count()) > 0) await modal.click({ timeout: Math.min(timeoutMs, 5_000) });
|
|
379
|
+
} catch {
|
|
380
|
+
// A modal that vanished between the count and the click needs no dismissal.
|
|
381
|
+
}
|
|
270
382
|
}
|
|
271
383
|
|
|
272
384
|
/** Live evidence: a visible submit toast, or a URL that moved off the URL the draft run left. */
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* InfoQ profile-session capability (R5, AC3).
|
|
3
3
|
*
|
|
4
|
-
* `login` opens the
|
|
5
|
-
* predicate passes
|
|
4
|
+
* `login` opens the InfoQ home in a headed browser and resolves only once the readiness
|
|
5
|
+
* predicate passes (hydrated write entry, no visible 登录 control); `check` reopens the profile
|
|
6
|
+
* headlessly and reports `verified` or `refused` — on InfoQ a headless pass always 451s at the
|
|
7
|
+
* WAF (measured 0201 + 0191), so a refusal is retried once headed.
|
|
6
8
|
* Arg parsing, profile precedence (`--profile` > `INFOQ_PROFILE_DIR` > `KK_BROWSER_PROFILE_DIR` >
|
|
7
9
|
* `~/.config/kk/browser-profiles/infoq`) and the exit codes (0 verified, 1 refused, 2 usage
|
|
8
10
|
* error) all live in the shared `runProfileCli` (0183), so the publish preflight and this CLI
|