@gobing-ai/knowledge-kit 0.0.31 → 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.
Files changed (79) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/dist/index.js +32320 -15021
  3. package/package.json +4 -4
  4. package/plugins/generations/content-gen/dist/index.js +10 -9
  5. package/plugins/generations/core-facts-gen/dist/index.js +13 -12
  6. package/plugins/generations/daily-article-gen/dist/index.js +14 -13
  7. package/plugins/generations/daily-article-gen/src/index.ts +1 -1
  8. package/plugins/generations/dailynews-gen/dist/index.js +10 -9
  9. package/plugins/generations/episode-plan-gen/dist/index.js +58 -15
  10. package/plugins/generations/episode-plan-gen/src/index.ts +76 -6
  11. package/plugins/generations/gemini-tts/README.md +11 -0
  12. package/plugins/generations/gemini-tts/dist/index.js +10 -9
  13. package/plugins/generations/image-gen/dist/index.js +154 -151
  14. package/plugins/generations/image-gen/src/compose/chromium.ts +60 -42
  15. package/plugins/generations/news-report-gen/dist/index.js +13 -12
  16. package/plugins/generations/voice-gen/dist/index.js +10 -9
  17. package/plugins/ingestions/aihot-ingest/dist/index.js +13 -12
  18. package/plugins/ingestions/horizon-ingest/dist/index.js +13 -12
  19. package/plugins/ingestions/karakeep-local/dist/index.js +13 -13
  20. package/plugins/ingestions/last30days-ingest/dist/index.js +13 -12
  21. package/plugins/ingestions/web-search/dist/index.js +11 -10
  22. package/plugins/kk/README.md +38 -0
  23. package/plugins/kk/decisions/extract/ai-news.yaml +21 -0
  24. package/plugins/kk/decisions/kk-items.yaml +113 -0
  25. package/plugins/kk/plugin.json +1 -1
  26. package/plugins/kk/schedules/cron.tmpl +15 -0
  27. package/plugins/kk/schedules/launchd.plist.tmpl +77 -0
  28. package/plugins/kk/scripts/daily-stages.ts +1533 -0
  29. package/plugins/kk/scripts/kk-workflow-stages.ts +268 -1228
  30. package/plugins/kk/skills/content-judge/SKILL.md +12 -2
  31. package/plugins/kk/skills/content-judge/references/rubrics.md +8 -2
  32. package/plugins/kk/skills/content-safety/SKILL.md +60 -18
  33. package/plugins/kk/skills/web-automation/SKILL.md +100 -0
  34. package/plugins/kk/skills/web-automation/references/browser-launch.md +86 -0
  35. package/plugins/kk/workflows/kk-collect.yaml +258 -0
  36. package/plugins/kk/workflows/kk-daily-ai-voice.yaml +74 -11
  37. package/plugins/kk/workflows/kk-daily-ai-voice2.yaml +655 -0
  38. package/plugins/kk/workflows/kk-daily-safety.yaml +170 -0
  39. package/plugins/publishings/emdash-pub/dist/index.js +10 -9
  40. package/plugins/publishings/infoq-pub/dist/index.js +8695 -211
  41. package/plugins/publishings/infoq-pub/package.json +3 -1
  42. package/plugins/publishings/infoq-pub/plugin.json +2 -1
  43. package/plugins/publishings/infoq-pub/src/index.ts +358 -29
  44. package/plugins/publishings/infoq-pub/src/profile-check.ts +21 -0
  45. package/plugins/publishings/juejin-pub/dist/index.js +8746 -211
  46. package/plugins/publishings/juejin-pub/package.json +3 -1
  47. package/plugins/publishings/juejin-pub/plugin.json +2 -1
  48. package/plugins/publishings/juejin-pub/src/index.ts +345 -29
  49. package/plugins/publishings/juejin-pub/src/profile-check.ts +19 -0
  50. package/plugins/publishings/medium-pub/dist/index.js +10 -9
  51. package/plugins/publishings/podcast-pub/dist/index.js +10 -9
  52. package/plugins/publishings/qiita-pub/dist/index.js +10 -9
  53. package/plugins/publishings/substack-pub/dist/index.js +8687 -211
  54. package/plugins/publishings/substack-pub/package.json +3 -1
  55. package/plugins/publishings/substack-pub/plugin.json +2 -1
  56. package/plugins/publishings/substack-pub/src/index.ts +302 -29
  57. package/plugins/publishings/substack-pub/src/profile-check.ts +76 -0
  58. package/plugins/publishings/surfdash-pub/dist/index.js +10 -9
  59. package/plugins/publishings/wechatmp-pub/dist/index.js +1315 -352
  60. package/plugins/publishings/wechatmp-pub/plugin.json +1 -0
  61. package/plugins/publishings/wechatmp-pub/src/enrich.ts +229 -0
  62. package/plugins/publishings/wechatmp-pub/src/flow.ts +896 -0
  63. package/plugins/publishings/wechatmp-pub/src/index.ts +223 -520
  64. package/plugins/publishings/wechatmp-pub/src/modes.ts +403 -0
  65. package/plugins/publishings/wechatmp-pub/src/page.ts +260 -0
  66. package/plugins/publishings/wechatmp-pub/src/profile-check.ts +84 -38
  67. package/plugins/publishings/x-pub/dist/index.js +8707 -223
  68. package/plugins/publishings/x-pub/package.json +3 -1
  69. package/plugins/publishings/x-pub/plugin.json +2 -1
  70. package/plugins/publishings/x-pub/src/index.ts +315 -23
  71. package/plugins/publishings/x-pub/src/profile-check.ts +19 -0
  72. package/plugins/publishings/xhs-pub/dist/index.js +897 -354
  73. package/plugins/publishings/xhs-pub/plugin.json +2 -1
  74. package/plugins/publishings/xhs-pub/src/flow.ts +572 -0
  75. package/plugins/publishings/xhs-pub/src/index.ts +108 -596
  76. package/plugins/publishings/xhs-pub/src/modes.ts +300 -0
  77. package/plugins/publishings/xhs-pub/src/page.ts +254 -0
  78. package/plugins/publishings/xhs-pub/src/profile-check.ts +29 -17
  79. package/plugins/publishings/zenn-pub/dist/index.js +10 -9
@@ -7,9 +7,11 @@
7
7
  },
8
8
  "dependencies": {
9
9
  "@gobing-ai/kk-core": "workspace:*",
10
+ "@gobing-ai/ts-browser-automation": "catalog:",
10
11
  "@gobing-ai/publish-harness": "workspace:*",
11
12
  "@gobing-ai/ts-runtime": "catalog:",
12
- "@gobing-ai/ts-utils": "catalog:"
13
+ "@gobing-ai/ts-utils": "catalog:",
14
+ "playwright": "catalog:"
13
15
  },
14
16
  "devDependencies": {
15
17
  "@types/bun": "1.3.14"
@@ -2,6 +2,7 @@
2
2
  "name": "juejin-pub",
3
3
  "kind": "publish",
4
4
  "entry": "./dist/index.js",
5
+ "check": "./src/profile-check.ts",
5
6
  "version": "1.0.0",
6
- "description": "Publish plugin for Juejin (juejin.cn) via browser/agent transport"
7
+ "description": "Publish plugin for Juejin (juejin.cn): article drafts through the bytemd editor via script browser transport (ts-browser-automation)"
7
8
  }
@@ -6,63 +6,379 @@ import {
6
6
  FakeBrowserTransport,
7
7
  type PublishTransport,
8
8
  type PublishTransportPayload,
9
- resolveBrowserProfileDir,
9
+ trySelectors,
10
10
  } from '@gobing-ai/publish-harness';
11
+ import {
12
+ type ChannelBrowserConfig,
13
+ checkChannelSession,
14
+ expectOutcome,
15
+ fillCodeMirror,
16
+ kkBrowserLauncher,
17
+ readScopedSignal,
18
+ resolveChannelProfileDir,
19
+ resolveSelector,
20
+ waitUntil,
21
+ } from '@gobing-ai/publish-harness/browser';
22
+ import {
23
+ type BrowserLauncher,
24
+ BrowserProfileMissingError,
25
+ type IsAuthenticatedPredicate,
26
+ LoginTimeoutError,
27
+ withProfilePage,
28
+ } from '@gobing-ai/ts-browser-automation';
11
29
  import { createNodeFileSystem } from '@gobing-ai/ts-runtime';
12
30
  import { echoError } from '@gobing-ai/ts-utils';
31
+ import type { Page } from 'playwright';
13
32
 
14
- const CHANNEL = 'juejin';
33
+ /** Channel slug: names the Result target and resolves the `JUEJIN_PROFILE_DIR` tier. */
34
+ export const CHANNEL = 'juejin';
15
35
  const TARGET = 'juejin';
36
+ /** Juejin's new-draft editor — the seeded URL the operator dogfood re-measures (R1). */
37
+ export const EDITOR_URL = 'https://juejin.cn/editor/drafts/new?v=2';
38
+ /** Login/SSO URLs that mean "not authenticated" — the editor never renders behind them. */
39
+ export const LOGIN_URL_RE = /(\/login|passport\.juejin\.cn)/;
40
+
41
+ /**
42
+ * Selectors measured against the Juejin editor (wt seed `juejin-playwright.ts:26-62`); each list is
43
+ * tried in order and the operator dogfood re-measures them. `publish` opens the live popup and
44
+ * `confirm` publishes it — both live-only (E91-D5).
45
+ */
46
+ export const SELECTORS = {
47
+ title: ['input.title-input', 'input[placeholder*="标题"]'],
48
+ body: ['.bytemd-editor .CodeMirror', '.CodeMirror', '.cm-editor'],
49
+ publish: ['button:has-text("发布")'],
50
+ category: '.category-list .item',
51
+ tagInput: '.tag-input input',
52
+ confirm: ['button:has-text("确定并发布")'],
53
+ } as const;
54
+
55
+ const STEP_TIMEOUT_MS = 30_000;
56
+ const LOGIN_TIMEOUT_MS = 180_000;
57
+ /** Preflight budget (0192): the headless editor pass is non-deterministic — it verified in 2.9 s
58
+ * for the CLI check but the identical readiness predicate burned 30 s in the publish preflight —
59
+ * so the preflight runs headed only with the measured hydration headroom (mirrors infoq 0191). */
60
+ const CHECK_TIMEOUT_MS = 60_000;
61
+ /** Juejin autosaves the draft on input: the design's window before the one manual save key press. */
62
+ const AUTOSAVE_TIMEOUT_MS = 5_000;
63
+ const SAVE_KEY = process.platform === 'darwin' ? 'Meta+s' : 'Control+s';
64
+ /** Operator recovery command every session failure names — never a cookie, page HTML or stack. */
65
+ const LOGIN_INSTRUCTION = 'bun plugins/publishings/juejin-pub/src/profile-check.ts login';
66
+ /** A saved draft lands on `/editor/drafts/<id>`; a live publish on `/post/<id>` or `/published/<id>`. */
67
+ const DRAFT_URL_RE = /\/editor\/drafts\/(\d+)/;
68
+ const LIVE_SIGNAL_RE = /juejin\.cn\/(post|published)\//;
69
+ const LIVE_URL_ID_RE = /juejin\.cn\/(?:post|published)\/(\d+)/;
70
+ const DRAFT_TOASTS = ['保存成功'];
71
+ const LIVE_TOASTS = ['发布成功'];
72
+
73
+ /**
74
+ * Juejin-owned readiness predicate: the editor is reachable (no login redirect) and the title field
75
+ * is attached. Shared by the publish preflight and the profile-session CLI (R1, R5).
76
+ */
77
+ export const isAuthenticated: IsAuthenticatedPredicate = async (page) =>
78
+ !LOGIN_URL_RE.test(page.url()) && (await trySelectors(page, SELECTORS.title, { visible: false })).found;
79
+
80
+ /** Site-owned config for the shared session check and operator CLI (0183). */
81
+ export const CONFIG: ChannelBrowserConfig = {
82
+ channel: CHANNEL,
83
+ homeUrl: EDITOR_URL,
84
+ isAuthenticated,
85
+ loginTimeoutMs: LOGIN_TIMEOUT_MS,
86
+ // The headless editor readiness is flaky (verified 2.9 s once, burned 30 s the next pass), so
87
+ // the profile-check CLI checks headed only instead of betting on a headless editor load.
88
+ checkHeadedOnly: true,
89
+ };
90
+
91
+ /** Validated live-run inputs: the publish popup cannot confirm without both (R4). */
92
+ interface LiveInputs {
93
+ category: string;
94
+ tags: string[];
95
+ }
96
+
97
+ /**
98
+ * One run shape: a draft run needs nothing beyond the content, a live run carries the validated
99
+ * popup inputs — so "live without a category or tags" is not a representable state.
100
+ */
101
+ type EditorRun =
102
+ | { publishStatus: 'draft'; profileDir: string }
103
+ | { publishStatus: 'public'; live: LiveInputs; profileDir: string };
16
104
 
17
105
  function fail(error: string): Result {
18
106
  return ResultSchema.parse({ ok: false, target: TARGET, error });
19
107
  }
20
108
 
21
- function draftId(content: Content): string {
22
- const title = content.title ?? 'untitled';
23
- return `${CHANNEL}-draft-${title.length}`;
109
+ function errorMessage(error: unknown): string {
110
+ return error instanceof Error ? error.message : String(error);
111
+ }
112
+
113
+ /** Session failures whose one-liner must name the recovery command (R5). */
114
+ function needsLogin(error: unknown): boolean {
115
+ return error instanceof LoginTimeoutError || error instanceof BrowserProfileMissingError;
24
116
  }
25
117
 
26
- function draftResult(content: Content, driver: BrowserDriver, profileDir: string): Result {
27
- return ResultSchema.parse({
28
- ok: true,
29
- target: TARGET,
30
- id: draftId(content),
31
- message: `${TARGET} draft prepared (unpublished)`,
32
- metadata: { driver, publishStatus: 'draft', profileDir },
33
- });
118
+ /**
119
+ * `确定并发布` needs a category and at least one tag, so a live run names the missing field instead
120
+ * of opening a browser it cannot finish in. Returns the validated inputs, or the failure message.
121
+ */
122
+ function liveInputs(content: Content): LiveInputs | string {
123
+ const category = content.metadata?.category;
124
+ if (typeof category !== 'string' || category.trim().length === 0) {
125
+ return 'live publish requires content.metadata.category (a Juejin category name)';
126
+ }
127
+ const raw = content.metadata?.tags;
128
+ const tags = Array.isArray(raw)
129
+ ? raw.filter((tag): tag is string => typeof tag === 'string' && tag.trim().length > 0)
130
+ : [];
131
+ // An absent, empty or partly unusable tag list is named as the missing field rather than
132
+ // silently trimmed down to the tags that happened to parse.
133
+ if (!Array.isArray(raw) || tags.length === 0 || tags.length !== raw.length) {
134
+ return 'live publish requires content.metadata.tags (at least one non-empty tag)';
135
+ }
136
+ return { category: category.trim(), tags };
137
+ }
138
+
139
+ /**
140
+ * Text of the first visible toast among `texts`, or null. `:text(…)` matches on a substring, so
141
+ * the element's own text is returned rather than the candidate literal.
142
+ */
143
+ /**
144
+ * The draft/live outcome, read only from the page's notification containers (residual 0196): an
145
+ * unscoped `:text(...)` match also hits the article body this flow filled a few steps earlier, so
146
+ * a body containing `保存成功` reported a draft that was never saved.
147
+ */
148
+ async function readToast(page: Page, texts: readonly string[]): Promise<string | null> {
149
+ return readScopedSignal(page, texts);
150
+ }
151
+
152
+ /**
153
+ * Declared modes (ADR-029 dispatch; task 0205 R2): Juejin's console flow is the CodeMirror markdown draft; publish tiers come from `publishStatus`, not modes. An unknown operation is refused by
154
+ * name before a profile is resolved or a browser opens.
155
+ */
156
+ export const JUEJIN_OPERATIONS = ['draft'] as const;
157
+ export type JuejinOperation = (typeof JUEJIN_OPERATIONS)[number];
158
+
159
+ /** Exit-early resolution of the requested operation; a default Content without one resolves to `draft`. */
160
+ export function resolveOperation(
161
+ options?: Record<string, unknown>,
162
+ content?: Content,
163
+ ): { operation: JuejinOperation; error?: undefined } | { operation?: undefined; error: string } {
164
+ const raw = options?.operation ?? content?.metadata?.operation;
165
+ if (raw === undefined) return { operation: 'draft' };
166
+ if (typeof raw !== 'string' || !(JUEJIN_OPERATIONS as readonly string[]).includes(raw)) {
167
+ return {
168
+ error: `unsupported operation '${String(raw)}' for juejin: expected one of ${JUEJIN_OPERATIONS.join(', ')}`,
169
+ };
170
+ }
171
+ return { operation: raw as JuejinOperation };
34
172
  }
35
173
 
36
174
  export class JuejinBrowserTransport implements PublishTransport {
37
175
  public readonly kind = 'browser';
38
176
  public readonly driver: BrowserDriver;
39
- private readonly loginTimeoutMs: number;
40
- private readonly sessionReady: boolean;
177
+ private readonly launcher: BrowserLauncher;
178
+ private readonly stepTimeoutMs: number;
179
+ private readonly checkTimeoutMs: number;
41
180
 
42
- constructor(driver: BrowserDriver = 'agent', opts?: { loginTimeoutMs?: number; sessionReady?: boolean }) {
181
+ constructor(
182
+ driver: BrowserDriver = 'script',
183
+ deps: { launcher?: BrowserLauncher; stepTimeoutMs?: number; checkTimeoutMs?: number } = {},
184
+ ) {
43
185
  this.driver = driver;
44
- this.loginTimeoutMs = opts?.loginTimeoutMs ?? 120_000;
45
- this.sessionReady = opts?.sessionReady ?? false;
186
+ this.launcher = deps.launcher ?? kkBrowserLauncher;
187
+ this.stepTimeoutMs = deps.stepTimeoutMs ?? STEP_TIMEOUT_MS;
188
+ this.checkTimeoutMs = deps.checkTimeoutMs ?? CHECK_TIMEOUT_MS;
46
189
  }
47
190
 
48
191
  public async publish(payload: PublishTransportPayload): Promise<Result> {
49
- const override =
50
- typeof payload.options?.profileDir === 'string'
51
- ? payload.options.profileDir
52
- : (process.env.JUEJIN_PROFILE_DIR ?? process.env.KK_BROWSER_PROFILE_DIR);
53
- const profileDir = resolveBrowserProfileDir(CHANNEL, override);
54
- if (!this.sessionReady) {
55
- return fail(
56
- `login timeout after ${this.loginTimeoutMs}ms: no authenticated ${CHANNEL} session in ${profileDir}`,
192
+ if (this.driver !== 'script') {
193
+ return fail(`unsupported driver '${this.driver}' for ${TARGET}`);
194
+ }
195
+ const mode = resolveOperation(payload.options, payload.content);
196
+ if (mode.error !== undefined) return fail(mode.error);
197
+ const requested = payload.content.metadata?.publishStatus;
198
+ if (requested !== undefined && requested !== 'draft' && requested !== 'public') {
199
+ return fail(`unknown publishStatus '${String(requested)}' (expected absent, 'draft' or 'public')`);
200
+ }
201
+ // Live pre-validation, before any browser opens: the popup's two prerequisites (R4).
202
+ let live: LiveInputs | undefined;
203
+ if (requested === 'public') {
204
+ const inputs = liveInputs(payload.content);
205
+ if (typeof inputs === 'string') return fail(inputs);
206
+ live = inputs;
207
+ }
208
+ const override = typeof payload.options?.profileDir === 'string' ? payload.options.profileDir : undefined;
209
+ const profileDir = resolveChannelProfileDir(CHANNEL, override);
210
+
211
+ // ADR-025 preflight: an unauthenticated or absent profile must never reach the editor
212
+ // flow, so the session check runs first and a refusal stays a fast refusal. Juejin's
213
+ // headless editor readiness is non-deterministic (0192: CLI check verified in 2.9 s, the
214
+ // same predicate refused after 30 s in this preflight), so — like infoq (0191) — the
215
+ // check runs headed only; a missing/expired profile still refuses before any flow.
216
+ const session = await checkChannelSession(CONFIG, {
217
+ profileDir,
218
+ launcher: this.launcher,
219
+ headedOnly: true,
220
+ timeoutMs: this.checkTimeoutMs,
221
+ });
222
+ if (session.status !== 'verified') {
223
+ return fail(`${CHANNEL} session preflight refused: ${session.detail} — run: ${LOGIN_INSTRUCTION}`);
224
+ }
225
+
226
+ const run: EditorRun =
227
+ live === undefined ? { publishStatus: 'draft', profileDir } : { publishStatus: 'public', live, profileDir };
228
+ try {
229
+ return await withProfilePage(
230
+ {
231
+ profileDir,
232
+ url: EDITOR_URL,
233
+ isAuthenticated,
234
+ // Headed by default (sites bot-wall headless loads; the preflight above is headed-only
235
+ // since 0192), `options.headless === true` the opt-in.
236
+ headless: payload.options?.headless === true,
237
+ launcher: this.launcher,
238
+ // The session is already preflight-verified, so the post-goto confirmation is
239
+ // a bounded step like every other one, not the 180s interactive login wait.
240
+ timeoutMs: this.stepTimeoutMs,
241
+ },
242
+ async ({ page }) => this.runEditorFlow(page, payload.content, run),
57
243
  );
244
+ } catch (error) {
245
+ const hint = needsLogin(error) ? ` — run: ${LOGIN_INSTRUCTION}` : '';
246
+ return fail(`${errorMessage(error)}${hint}`);
58
247
  }
59
- return draftResult(payload.content, this.driver, profileDir);
60
248
  }
249
+
250
+ /**
251
+ * The site-owned editor flow: title → markdown body → draft signal, then — for a live run only
252
+ * — `发布` → category → tags → `确定并发布` → live signal. Every outcome comes from an
253
+ * `expectOutcome` probe, so a wait or a click without an observed signal is never a success
254
+ * (E91-D6), and a draft run never touches a live control (E91-D5).
255
+ */
256
+ private async runEditorFlow(page: Page, content: Content, run: EditorRun): Promise<Result> {
257
+ const metadata = { driver: this.driver, publishStatus: run.publishStatus, profileDir: run.profileDir };
258
+
259
+ const titleSelector = await resolveSelector(page, SELECTORS.title, {
260
+ timeoutMs: this.stepTimeoutMs,
261
+ label: 'title input',
262
+ });
263
+ await page
264
+ .locator(titleSelector)
265
+ .first()
266
+ .fill(content.title ?? '');
267
+
268
+ const bodySelector = await resolveSelector(page, SELECTORS.body, {
269
+ timeoutMs: this.stepTimeoutMs,
270
+ label: 'body editor',
271
+ });
272
+ // bytemd is CodeMirror over markdown: the body goes in as markdown, never as HTML (E91-D4).
273
+ await fillCodeMirror(page, bodySelector, content.body, { timeoutMs: this.stepTimeoutMs });
274
+
275
+ // Draft evidence (E91-D6): the autosave gets its window, then the manual save key is pressed
276
+ // once when the page showed nothing. Success still needs the observed signal.
277
+ const autosaveMs = Math.min(AUTOSAVE_TIMEOUT_MS, this.stepTimeoutMs);
278
+ if (!(await waitUntil(async () => (await probeDraft(page)) !== null, autosaveMs))) {
279
+ await page.keyboard.press(SAVE_KEY);
280
+ }
281
+ const draft = await expectOutcome(() => probeDraft(page), {
282
+ timeoutMs: this.stepTimeoutMs,
283
+ label: `juejin draft save in '${bodySelector}'`,
284
+ });
285
+ if (!draft.ok) return fail(draft.error);
286
+
287
+ if (run.publishStatus === 'draft') {
288
+ const url = page.url();
289
+ const id = idFromUrl(url);
290
+ return ResultSchema.parse({
291
+ ok: true,
292
+ target: TARGET,
293
+ ...(id === undefined ? {} : { id }),
294
+ url,
295
+ message: `${TARGET} draft saved (unpublished) in profile ${run.profileDir}`,
296
+ metadata,
297
+ });
298
+ }
299
+
300
+ const publishSelector = await resolveSelector(page, SELECTORS.publish, {
301
+ timeoutMs: this.stepTimeoutMs,
302
+ label: 'publish control',
303
+ });
304
+ await page.locator(publishSelector).first().click();
305
+ await fillPublishPopup(page, run.live, this.stepTimeoutMs);
306
+
307
+ const confirmSelector = await resolveSelector(page, SELECTORS.confirm, {
308
+ timeoutMs: this.stepTimeoutMs,
309
+ label: 'confirm control',
310
+ });
311
+ await page.locator(confirmSelector).first().click();
312
+ const live = await expectOutcome(() => probeLive(page), {
313
+ timeoutMs: this.stepTimeoutMs,
314
+ label: `juejin publish '${confirmSelector}'`,
315
+ });
316
+ if (!live.ok) return fail(live.error);
317
+
318
+ const url = page.url();
319
+ const id = idFromUrl(url);
320
+ return ResultSchema.parse({
321
+ ok: true,
322
+ target: TARGET,
323
+ ...(id === undefined ? {} : { id }),
324
+ url,
325
+ message: `${TARGET} published live in profile ${run.profileDir}`,
326
+ metadata,
327
+ });
328
+ }
329
+ }
330
+
331
+ /**
332
+ * Live popup (E91-D5): the category item whose text is exactly `metadata.category`, then every tag
333
+ * typed into the tag input with `Enter`. Runs only on the `public` path — `发布` opens this popup.
334
+ */
335
+ async function fillPublishPopup(page: Page, live: LiveInputs, timeoutMs: number): Promise<void> {
336
+ await resolveSelector(page, [SELECTORS.category], { timeoutMs, label: 'category list' });
337
+ const items = page.locator(SELECTORS.category);
338
+ const total = await items.count();
339
+ let picked = false;
340
+ for (let index = 0; index < total; index += 1) {
341
+ const item = items.nth(index);
342
+ if (((await item.textContent()) ?? '').trim() === live.category) {
343
+ await item.click();
344
+ picked = true;
345
+ break;
346
+ }
347
+ }
348
+ if (!picked) {
349
+ throw new Error(`category '${live.category}': no '${SELECTORS.category}' item matched its exact text`);
350
+ }
351
+
352
+ const tagSelector = await resolveSelector(page, [SELECTORS.tagInput], { timeoutMs, label: 'tag input' });
353
+ for (const tag of live.tags) {
354
+ const input = page.locator(tagSelector).first();
355
+ await input.fill(tag);
356
+ await input.press('Enter');
357
+ }
358
+ }
359
+
360
+ /** The article id the observed URL carries: `/editor/drafts/<id>` (draft) or `/post/<id>` (live). */
361
+ function idFromUrl(url: string): string | undefined {
362
+ return DRAFT_URL_RE.exec(url)?.[1] ?? LIVE_URL_ID_RE.exec(url)?.[1];
363
+ }
364
+
365
+ /** Draft evidence (E91-D6): the editor URL moved onto a draft id, or a visible `保存成功` toast. */
366
+ async function probeDraft(page: Page): Promise<string | null> {
367
+ const url = page.url();
368
+ if (DRAFT_URL_RE.test(url)) return url;
369
+ return readToast(page, DRAFT_TOASTS);
370
+ }
371
+
372
+ /** Live evidence (E91-D6): a `/post/`- or `/published/`-shaped URL, or a visible `发布成功` toast. */
373
+ async function probeLive(page: Page): Promise<string | null> {
374
+ const url = page.url();
375
+ if (LIVE_SIGNAL_RE.test(url)) return url;
376
+ return readToast(page, LIVE_TOASTS);
61
377
  }
62
378
 
63
- export function getPublishTransport(driver: BrowserDriver = 'agent'): PublishTransport {
379
+ export function getPublishTransport(driver: BrowserDriver = 'script'): PublishTransport {
64
380
  if (process.env.KNOWLEDGE_KIT_PUBLISH_TRANSPORT === 'fake') {
65
- return new FakeBrowserTransport(driver);
381
+ return new FakeBrowserTransport('script');
66
382
  }
67
383
  return new JuejinBrowserTransport(driver);
68
384
  }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Juejin profile-session capability (R5, AC3).
3
+ *
4
+ * `login` opens the editor in a headed browser and resolves only once Juejin's readiness predicate
5
+ * passes; `check` reopens the profile headlessly and reports `verified` or `refused`. Arg parsing,
6
+ * profile precedence (`--profile` > `JUEJIN_PROFILE_DIR` > `KK_BROWSER_PROFILE_DIR` >
7
+ * `~/.config/kk/browser-profiles/juejin`) and the exit codes (0 verified, 1 refused, 2 usage error)
8
+ * all live in the shared `runProfileCli` (0183), so the publish preflight and this CLI apply the
9
+ * same gate.
10
+ *
11
+ * Operator dogfood (plan step 6, handed to 0192): run `profile-check.ts login`, then `check`, on
12
+ * the same profile, and re-measure the editor URL, the selectors and the draft signal.
13
+ */
14
+ import { runProfileCli } from '@gobing-ai/publish-harness/browser';
15
+ import { CONFIG } from './index';
16
+
17
+ if (import.meta.main) {
18
+ process.exit(await runProfileCli(CONFIG, Bun.argv.slice(2)));
19
+ }
@@ -14642,7 +14642,7 @@ var init_zod = __esm(() => {
14642
14642
  init_external();
14643
14643
  });
14644
14644
 
14645
- // ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.11/node_modules/@gobing-ai/ts-utils/dist/errors.js
14645
+ // ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.18/node_modules/@gobing-ai/ts-utils/dist/errors.js
14646
14646
  var ErrorCode;
14647
14647
  var init_errors3 = __esm(() => {
14648
14648
  ErrorCode = {
@@ -14653,7 +14653,7 @@ var init_errors3 = __esm(() => {
14653
14653
  };
14654
14654
  });
14655
14655
 
14656
- // ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.11/node_modules/@gobing-ai/ts-utils/dist/api-response.js
14656
+ // ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.18/node_modules/@gobing-ai/ts-utils/dist/api-response.js
14657
14657
  var API_ERROR_CODES, ERROR_CODE_TO_HTTP, CLIENT_SAFE_CODES;
14658
14658
  var init_api_response = __esm(() => {
14659
14659
  init_errors3();
@@ -14676,13 +14676,13 @@ var init_api_response = __esm(() => {
14676
14676
  CLIENT_SAFE_CODES = new Set([ErrorCode.NotFound, ErrorCode.Validation, ErrorCode.Conflict]);
14677
14677
  });
14678
14678
 
14679
- // ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.11/node_modules/@gobing-ai/ts-utils/dist/cursor.js
14679
+ // ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.18/node_modules/@gobing-ai/ts-utils/dist/cursor.js
14680
14680
  var init_cursor = () => {};
14681
14681
 
14682
- // ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.11/node_modules/@gobing-ai/ts-utils/dist/event-severity.js
14682
+ // ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.18/node_modules/@gobing-ai/ts-utils/dist/event-severity.js
14683
14683
  var init_event_severity = () => {};
14684
14684
 
14685
- // ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.11/node_modules/@gobing-ai/ts-utils/dist/output.js
14685
+ // ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.18/node_modules/@gobing-ai/ts-utils/dist/output.js
14686
14686
  function processStream(name) {
14687
14687
  const proc = globalThis.process;
14688
14688
  const stream = proc?.[name];
@@ -14700,7 +14700,7 @@ function echoError(message, target = defaultStderrTarget ?? processStream("stder
14700
14700
  }
14701
14701
  var defaultStderrTarget;
14702
14702
 
14703
- // ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.11/node_modules/@gobing-ai/ts-utils/dist/index.js
14703
+ // ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.18/node_modules/@gobing-ai/ts-utils/dist/index.js
14704
14704
  var init_dist = __esm(() => {
14705
14705
  init_api_response();
14706
14706
  init_cursor();
@@ -21698,7 +21698,7 @@ var init_dist2 = __esm(() => {
21698
21698
  $visitAsync = visit.visitAsync;
21699
21699
  });
21700
21700
 
21701
- // ../../node_modules/.bun/@gobing-ai+ts-runtime@0.5.11/node_modules/@gobing-ai/ts-runtime/dist/config.js
21701
+ // ../../node_modules/.bun/@gobing-ai+ts-runtime@0.5.18+ea28d47622d670ff/node_modules/@gobing-ai/ts-runtime/dist/config.js
21702
21702
  var configSchema;
21703
21703
  var init_config = __esm(() => {
21704
21704
  init_dist2();
@@ -21722,7 +21722,7 @@ var init_config = __esm(() => {
21722
21722
  });
21723
21723
  });
21724
21724
 
21725
- // ../../node_modules/.bun/@gobing-ai+ts-runtime@0.5.11/node_modules/@gobing-ai/ts-runtime/dist/file-system-node.js
21725
+ // ../../node_modules/.bun/@gobing-ai+ts-runtime@0.5.18+ea28d47622d670ff/node_modules/@gobing-ai/ts-runtime/dist/file-system-node.js
21726
21726
  import { appendFileSync, cpSync, createReadStream, createWriteStream, existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from "fs";
21727
21727
  import { dirname, resolve as resolvePath } from "path";
21728
21728
  function createNodeFileSystem(root, paths) {
@@ -21896,11 +21896,12 @@ var PluginManifestSchema = exports_external.looseObject({
21896
21896
  name: exports_external.string().min(1),
21897
21897
  kind: PluginKindSchema,
21898
21898
  entry: exports_external.string().min(1),
21899
+ check: exports_external.string().min(1).optional(),
21899
21900
  version: exports_external.string().optional(),
21900
21901
  description: exports_external.string().optional(),
21901
21902
  operations: exports_external.record(exports_external.string(), PluginOperationSchema).optional()
21902
21903
  });
21903
- // ../../node_modules/.bun/@gobing-ai+ts-runtime@0.5.11/node_modules/@gobing-ai/ts-runtime/dist/index.js
21904
+ // ../../node_modules/.bun/@gobing-ai+ts-runtime@0.5.18+ea28d47622d670ff/node_modules/@gobing-ai/ts-runtime/dist/index.js
21904
21905
  init_file_system_node();
21905
21906
  init_config();
21906
21907
 
@@ -14642,7 +14642,7 @@ var init_zod = __esm(() => {
14642
14642
  init_external();
14643
14643
  });
14644
14644
 
14645
- // ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.11/node_modules/@gobing-ai/ts-utils/dist/errors.js
14645
+ // ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.18/node_modules/@gobing-ai/ts-utils/dist/errors.js
14646
14646
  var ErrorCode;
14647
14647
  var init_errors3 = __esm(() => {
14648
14648
  ErrorCode = {
@@ -14653,7 +14653,7 @@ var init_errors3 = __esm(() => {
14653
14653
  };
14654
14654
  });
14655
14655
 
14656
- // ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.11/node_modules/@gobing-ai/ts-utils/dist/api-response.js
14656
+ // ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.18/node_modules/@gobing-ai/ts-utils/dist/api-response.js
14657
14657
  var API_ERROR_CODES, ERROR_CODE_TO_HTTP, CLIENT_SAFE_CODES;
14658
14658
  var init_api_response = __esm(() => {
14659
14659
  init_errors3();
@@ -14676,13 +14676,13 @@ var init_api_response = __esm(() => {
14676
14676
  CLIENT_SAFE_CODES = new Set([ErrorCode.NotFound, ErrorCode.Validation, ErrorCode.Conflict]);
14677
14677
  });
14678
14678
 
14679
- // ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.11/node_modules/@gobing-ai/ts-utils/dist/cursor.js
14679
+ // ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.18/node_modules/@gobing-ai/ts-utils/dist/cursor.js
14680
14680
  var init_cursor = () => {};
14681
14681
 
14682
- // ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.11/node_modules/@gobing-ai/ts-utils/dist/event-severity.js
14682
+ // ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.18/node_modules/@gobing-ai/ts-utils/dist/event-severity.js
14683
14683
  var init_event_severity = () => {};
14684
14684
 
14685
- // ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.11/node_modules/@gobing-ai/ts-utils/dist/output.js
14685
+ // ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.18/node_modules/@gobing-ai/ts-utils/dist/output.js
14686
14686
  function processStream(name) {
14687
14687
  const proc = globalThis.process;
14688
14688
  const stream = proc?.[name];
@@ -14700,7 +14700,7 @@ function echoError(message, target = defaultStderrTarget ?? processStream("stder
14700
14700
  }
14701
14701
  var defaultStderrTarget;
14702
14702
 
14703
- // ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.11/node_modules/@gobing-ai/ts-utils/dist/index.js
14703
+ // ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.18/node_modules/@gobing-ai/ts-utils/dist/index.js
14704
14704
  var init_dist = __esm(() => {
14705
14705
  init_api_response();
14706
14706
  init_cursor();
@@ -21698,7 +21698,7 @@ var init_dist2 = __esm(() => {
21698
21698
  $visitAsync = visit.visitAsync;
21699
21699
  });
21700
21700
 
21701
- // ../../node_modules/.bun/@gobing-ai+ts-runtime@0.5.11/node_modules/@gobing-ai/ts-runtime/dist/config.js
21701
+ // ../../node_modules/.bun/@gobing-ai+ts-runtime@0.5.18+ea28d47622d670ff/node_modules/@gobing-ai/ts-runtime/dist/config.js
21702
21702
  var configSchema;
21703
21703
  var init_config = __esm(() => {
21704
21704
  init_dist2();
@@ -21722,7 +21722,7 @@ var init_config = __esm(() => {
21722
21722
  });
21723
21723
  });
21724
21724
 
21725
- // ../../node_modules/.bun/@gobing-ai+ts-runtime@0.5.11/node_modules/@gobing-ai/ts-runtime/dist/file-system-node.js
21725
+ // ../../node_modules/.bun/@gobing-ai+ts-runtime@0.5.18+ea28d47622d670ff/node_modules/@gobing-ai/ts-runtime/dist/file-system-node.js
21726
21726
  import { appendFileSync, cpSync, createReadStream, createWriteStream, existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from "fs";
21727
21727
  import { dirname, resolve as resolvePath } from "path";
21728
21728
  function createNodeFileSystem(root, paths) {
@@ -21898,11 +21898,12 @@ var PluginManifestSchema = exports_external.looseObject({
21898
21898
  name: exports_external.string().min(1),
21899
21899
  kind: PluginKindSchema,
21900
21900
  entry: exports_external.string().min(1),
21901
+ check: exports_external.string().min(1).optional(),
21901
21902
  version: exports_external.string().optional(),
21902
21903
  description: exports_external.string().optional(),
21903
21904
  operations: exports_external.record(exports_external.string(), PluginOperationSchema).optional()
21904
21905
  });
21905
- // ../../node_modules/.bun/@gobing-ai+ts-runtime@0.5.11/node_modules/@gobing-ai/ts-runtime/dist/index.js
21906
+ // ../../node_modules/.bun/@gobing-ai+ts-runtime@0.5.18+ea28d47622d670ff/node_modules/@gobing-ai/ts-runtime/dist/index.js
21906
21907
  init_file_system_node();
21907
21908
  init_config();
21908
21909