@gobing-ai/knowledge-kit 0.1.10 → 0.1.12

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 (158) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/dist/index.js +2478 -1737
  3. package/package.json +1 -1
  4. package/plugins/generations/image-gen/README.md +108 -0
  5. package/plugins/generations/image-gen/assets/sample/cover__clean-webapp-ui.jpg +0 -0
  6. package/plugins/generations/image-gen/assets/sample/cover__cute.jpg +0 -0
  7. package/plugins/generations/image-gen/assets/sample/cover__daily-news.jpg +0 -0
  8. package/plugins/generations/image-gen/assets/sample/cover__editorial.jpg +0 -0
  9. package/plugins/generations/image-gen/assets/sample/cover__fresh.jpg +0 -0
  10. package/plugins/generations/image-gen/assets/sample/cover__minimalist.jpg +0 -0
  11. package/plugins/generations/image-gen/assets/sample/cover__photorealistic.jpg +0 -0
  12. package/plugins/generations/image-gen/assets/sample/cover__sketch.jpg +0 -0
  13. package/plugins/generations/image-gen/assets/sample/cover__technical-diagram.jpg +0 -0
  14. package/plugins/generations/image-gen/assets/sample/cover__vibrant.jpg +0 -0
  15. package/plugins/generations/image-gen/assets/sample/cover__warm.jpg +0 -0
  16. package/plugins/generations/image-gen/assets/sample/cover__xhs-knowledge.jpg +0 -0
  17. package/plugins/generations/image-gen/assets/sample/daily-news-cover__clean-webapp-ui.jpg +0 -0
  18. package/plugins/generations/image-gen/assets/sample/daily-news-cover__cute.jpg +0 -0
  19. package/plugins/generations/image-gen/assets/sample/daily-news-cover__daily-news.jpg +0 -0
  20. package/plugins/generations/image-gen/assets/sample/daily-news-cover__editorial.jpg +0 -0
  21. package/plugins/generations/image-gen/assets/sample/daily-news-cover__fresh.jpg +0 -0
  22. package/plugins/generations/image-gen/assets/sample/daily-news-cover__minimalist.jpg +0 -0
  23. package/plugins/generations/image-gen/assets/sample/daily-news-cover__photorealistic.jpg +0 -0
  24. package/plugins/generations/image-gen/assets/sample/daily-news-cover__sketch.jpg +0 -0
  25. package/plugins/generations/image-gen/assets/sample/daily-news-cover__technical-diagram.jpg +0 -0
  26. package/plugins/generations/image-gen/assets/sample/daily-news-cover__vibrant.jpg +0 -0
  27. package/plugins/generations/image-gen/assets/sample/daily-news-cover__warm.jpg +0 -0
  28. package/plugins/generations/image-gen/assets/sample/daily-news-cover__xhs-knowledge.jpg +0 -0
  29. package/plugins/generations/image-gen/assets/sample/free__clean-webapp-ui.jpg +0 -0
  30. package/plugins/generations/image-gen/assets/sample/free__cute.jpg +0 -0
  31. package/plugins/generations/image-gen/assets/sample/free__daily-news.jpg +0 -0
  32. package/plugins/generations/image-gen/assets/sample/free__editorial.jpg +0 -0
  33. package/plugins/generations/image-gen/assets/sample/free__fresh.jpg +0 -0
  34. package/plugins/generations/image-gen/assets/sample/free__minimalist.jpg +0 -0
  35. package/plugins/generations/image-gen/assets/sample/free__photorealistic.jpg +0 -0
  36. package/plugins/generations/image-gen/assets/sample/free__sketch.jpg +0 -0
  37. package/plugins/generations/image-gen/assets/sample/free__technical-diagram.jpg +0 -0
  38. package/plugins/generations/image-gen/assets/sample/free__vibrant.jpg +0 -0
  39. package/plugins/generations/image-gen/assets/sample/free__warm.jpg +0 -0
  40. package/plugins/generations/image-gen/assets/sample/free__xhs-knowledge.jpg +0 -0
  41. package/plugins/generations/image-gen/assets/sample/illustration__clean-webapp-ui.jpg +0 -0
  42. package/plugins/generations/image-gen/assets/sample/illustration__cute.jpg +0 -0
  43. package/plugins/generations/image-gen/assets/sample/illustration__daily-news.jpg +0 -0
  44. package/plugins/generations/image-gen/assets/sample/illustration__editorial.jpg +0 -0
  45. package/plugins/generations/image-gen/assets/sample/illustration__fresh.jpg +0 -0
  46. package/plugins/generations/image-gen/assets/sample/illustration__minimalist.jpg +0 -0
  47. package/plugins/generations/image-gen/assets/sample/illustration__photorealistic.jpg +0 -0
  48. package/plugins/generations/image-gen/assets/sample/illustration__sketch.jpg +0 -0
  49. package/plugins/generations/image-gen/assets/sample/illustration__technical-diagram.jpg +0 -0
  50. package/plugins/generations/image-gen/assets/sample/illustration__vibrant.jpg +0 -0
  51. package/plugins/generations/image-gen/assets/sample/illustration__warm.jpg +0 -0
  52. package/plugins/generations/image-gen/assets/sample/illustration__xhs-knowledge.jpg +0 -0
  53. package/plugins/generations/image-gen/assets/sample/xhs-card__clean-webapp-ui.jpg +0 -0
  54. package/plugins/generations/image-gen/assets/sample/xhs-card__cute.jpg +0 -0
  55. package/plugins/generations/image-gen/assets/sample/xhs-card__daily-news.jpg +0 -0
  56. package/plugins/generations/image-gen/assets/sample/xhs-card__editorial.jpg +0 -0
  57. package/plugins/generations/image-gen/assets/sample/xhs-card__fresh.jpg +0 -0
  58. package/plugins/generations/image-gen/assets/sample/xhs-card__minimalist.jpg +0 -0
  59. package/plugins/generations/image-gen/assets/sample/xhs-card__photorealistic.jpg +0 -0
  60. package/plugins/generations/image-gen/assets/sample/xhs-card__sketch.jpg +0 -0
  61. package/plugins/generations/image-gen/assets/sample/xhs-card__technical-diagram.jpg +0 -0
  62. package/plugins/generations/image-gen/assets/sample/xhs-card__vibrant.jpg +0 -0
  63. package/plugins/generations/image-gen/assets/sample/xhs-card__warm.jpg +0 -0
  64. package/plugins/generations/image-gen/assets/sample/xhs-card__xhs-knowledge.jpg +0 -0
  65. package/plugins/generations/image-gen/dist/index.js +102 -58
  66. package/plugins/generations/image-gen/scripts/compose-e2e-artifact.ts +5 -21
  67. package/plugins/generations/image-gen/scripts/gen-samples.ts +208 -0
  68. package/plugins/generations/image-gen/src/compose/chromium.ts +2 -3
  69. package/plugins/generations/image-gen/src/presets.ts +6 -3
  70. package/plugins/generations/image-gen/src/providers/errors.ts +16 -1
  71. package/plugins/generations/image-gen/src/providers/minimax.ts +46 -17
  72. package/plugins/ingestions/web-search/src/index.ts +1 -1
  73. package/plugins/kk/README.md +18 -0
  74. package/plugins/kk/commands/image-extract.md +1 -0
  75. package/plugins/kk/commands/image-generate.md +1 -0
  76. package/plugins/kk/commands/tell-me.md +1 -0
  77. package/plugins/kk/commands/workflow-run.md +7 -1
  78. package/plugins/kk/decisions/extract/it-topics.yaml +24 -0
  79. package/plugins/kk/decisions/kk-itc.yaml +6 -4
  80. package/plugins/kk/plugin.json +1 -1
  81. package/plugins/kk/publish-presets/daily-ai-voice-migration.json +146 -0
  82. package/plugins/kk/publish-presets/daily-ai-voice.json +11 -3
  83. package/plugins/kk/schedules/kk-collect.grok-bot.md.tmpl +69 -0
  84. package/plugins/kk/schedules/kk-daily-ai-voice3.cron.tmpl +18 -0
  85. package/plugins/kk/schedules/kk-daily-publish3-cn-peak.cron.tmpl +15 -5
  86. package/plugins/kk/schedules/kk-daily-publish3-overseas.cron.tmpl +9 -4
  87. package/plugins/kk/scripts/daily-scheduled-run.ts +298 -0
  88. package/plugins/kk/scripts/daily-stages.ts +1403 -156
  89. package/plugins/kk/scripts/kk-workflow-stages.ts +266 -10
  90. package/plugins/kk/scripts/safety-artifact-check.ts +277 -0
  91. package/plugins/kk/scripts/voice3-publish-window.ts +202 -0
  92. package/plugins/kk/skills/article-adapt/SKILL.md +1 -0
  93. package/plugins/kk/skills/audio-authoring/SKILL.md +1 -0
  94. package/plugins/kk/skills/content-judge/SKILL.md +1 -0
  95. package/plugins/kk/skills/content-safety/SKILL.md +1 -0
  96. package/plugins/kk/skills/explain-things/SKILL.md +1 -0
  97. package/plugins/kk/skills/image-authoring/SKILL.md +1 -0
  98. package/plugins/kk/skills/itc-generating/SKILL.md +1 -0
  99. package/plugins/kk/skills/publish/SKILL.md +1 -0
  100. package/plugins/kk/skills/smart-seo/SKILL.md +1 -0
  101. package/plugins/kk/skills/storm-research/SKILL.md +1 -0
  102. package/plugins/kk/skills/taste-unslop/SKILL.md +1 -0
  103. package/plugins/kk/skills/topic/SKILL.md +1 -0
  104. package/plugins/kk/skills/web-automation/SKILL.md +1 -0
  105. package/plugins/kk/skills/xhs-images/SKILL.md +1 -0
  106. package/plugins/kk/workflows/kk-daily-ai-voice2.yaml +177 -128
  107. package/plugins/kk/workflows/kk-daily-ai-voice3.yaml +161 -61
  108. package/plugins/kk/workflows/kk-daily-publish3.yaml +82 -26
  109. package/plugins/kk/workflows/kk-daily-safety.yaml +156 -69
  110. package/plugins/kk/workflows/kk-itc-author.yaml +75 -0
  111. package/plugins/kk/workflows/kk-itc-publish-tail.yaml +25 -3
  112. package/plugins/kk/workflows/kk-itc.yaml +36 -4
  113. package/plugins/publishings/bilibili-pub/dist/index.js +2 -1
  114. package/plugins/publishings/bilibili-pub/dist/profile-check.js +2 -1
  115. package/plugins/publishings/cnblogs-pub/dist/index.js +2 -1
  116. package/plugins/publishings/cnblogs-pub/dist/profile-check.js +2 -1
  117. package/plugins/publishings/csdn-pub/dist/index.js +2 -1
  118. package/plugins/publishings/csdn-pub/dist/profile-check.js +2 -1
  119. package/plugins/publishings/infoq-pub/dist/index.js +2 -1
  120. package/plugins/publishings/infoq-pub/dist/profile-check.js +2 -1
  121. package/plugins/publishings/jike-pub/dist/index.js +2 -1
  122. package/plugins/publishings/jike-pub/dist/profile-check.js +2 -1
  123. package/plugins/publishings/juejin-pub/dist/index.js +2 -1
  124. package/plugins/publishings/juejin-pub/dist/profile-check.js +2 -1
  125. package/plugins/publishings/linkedin-pub/dist/index.js +21333 -0
  126. package/plugins/publishings/linkedin-pub/dist/profile-check.js +21005 -0
  127. package/plugins/publishings/linkedin-pub/package.json +19 -0
  128. package/plugins/publishings/linkedin-pub/plugin.json +12 -0
  129. package/plugins/publishings/linkedin-pub/src/index.ts +705 -0
  130. package/plugins/publishings/linkedin-pub/src/profile-check.ts +17 -0
  131. package/plugins/publishings/linkedin-pub/tsconfig.json +4 -0
  132. package/plugins/publishings/podcast-pub/dist/index.js +46 -8
  133. package/plugins/publishings/podcast-pub/src/index.ts +62 -5
  134. package/plugins/publishings/shipinhao-pub/dist/index.js +2 -1
  135. package/plugins/publishings/shipinhao-pub/dist/profile-check.js +2 -1
  136. package/plugins/publishings/sspai-pub/dist/index.js +2 -1
  137. package/plugins/publishings/sspai-pub/dist/profile-check.js +2 -1
  138. package/plugins/publishings/substack-pub/dist/index.js +2 -1
  139. package/plugins/publishings/substack-pub/dist/profile-check.js +2 -1
  140. package/plugins/publishings/surfdash-pub/dist/index.js +461 -53
  141. package/plugins/publishings/surfdash-pub/src/digest-check.ts +85 -19
  142. package/plugins/publishings/surfdash-pub/src/index.ts +592 -65
  143. package/plugins/publishings/threads-pub/dist/index.js +7 -0
  144. package/plugins/publishings/threads-pub/src/index.ts +12 -0
  145. package/plugins/publishings/wechatmp-pub/dist/index.js +936 -98
  146. package/plugins/publishings/wechatmp-pub/dist/profile-check.js +60 -3
  147. package/plugins/publishings/wechatmp-pub/src/flow.ts +1134 -86
  148. package/plugins/publishings/wechatmp-pub/src/index.ts +112 -30
  149. package/plugins/publishings/wechatmp-pub/src/page.ts +105 -0
  150. package/plugins/publishings/weibo-pub/dist/index.js +2 -1
  151. package/plugins/publishings/weibo-pub/dist/profile-check.js +2 -1
  152. package/plugins/publishings/x-pub/dist/index.js +9 -1
  153. package/plugins/publishings/x-pub/dist/profile-check.js +2 -1
  154. package/plugins/publishings/x-pub/src/index.ts +11 -0
  155. package/plugins/publishings/xhs-pub/dist/index.js +2 -1
  156. package/plugins/publishings/xhs-pub/dist/profile-check.js +2 -1
  157. package/plugins/publishings/xiaoyuzhou-pub/dist/index.js +2 -1
  158. package/plugins/publishings/xiaoyuzhou-pub/dist/profile-check.js +2 -1
@@ -1,3 +1,4 @@
1
+ import { randomUUID } from 'node:crypto';
1
2
  import { parseArgs } from 'node:util';
2
3
  import { type Content, ContentSchema, type Result, ResultSchema } from '@gobing-ai/kk-core';
3
4
  import {
@@ -6,7 +7,7 @@ import {
6
7
  type PublishTransportPayload,
7
8
  resolveBrowserProfileDir,
8
9
  } from '@gobing-ai/publish-harness';
9
- import { kkBrowserLauncher, waitUntil } from '@gobing-ai/publish-harness/browser';
10
+ import { chmod, createExclusiveFile, kkBrowserLauncher, waitUntil } from '@gobing-ai/publish-harness/browser';
10
11
  import { BrowserProfileMissingError, openPersistentBrowser } from '@gobing-ai/ts-browser-automation';
11
12
  import { createNodeFileSystem } from '@gobing-ai/ts-runtime';
12
13
  import { echoError } from '@gobing-ai/ts-utils';
@@ -21,7 +22,14 @@ import {
21
22
  validateModeInput,
22
23
  WECHATMP_OPERATIONS,
23
24
  } from './modes';
24
- import { type OpenConsole, type OpenedConsole, POLL_MS, STEP_TIMEOUT_MS, wrapPlaywrightContext } from './page';
25
+ import {
26
+ type OpenConsole,
27
+ type OpenedConsole,
28
+ POLL_MS,
29
+ STEP_TIMEOUT_MS,
30
+ sanitizeSessionEvidence,
31
+ wrapPlaywrightContext,
32
+ } from './page';
25
33
 
26
34
  /**
27
35
  * The wechatmp publish CHANNEL (ADR-029, ADR-026): one plugin, four mode `operations`.
@@ -49,7 +57,7 @@ const DRAFT_SIGNAL_TIMEOUT_MS = 15_000;
49
57
  const SUBMIT_ATTEMPTS = 2;
50
58
 
51
59
  function fail(error: string): Result {
52
- return ResultSchema.parse({ ok: false, target: TARGET, error });
60
+ return ResultSchema.parse({ ok: false, target: TARGET, error: sanitizeSessionEvidence(error) });
53
61
  }
54
62
 
55
63
  /** Console home readiness: an authenticated route carries the session token (discovery §1/§9-W9). */
@@ -74,6 +82,35 @@ export function storageStatePath(profileDir: string): string {
74
82
  return `${profileDir}/storage-state.json`;
75
83
  }
76
84
 
85
+ /**
86
+ * Persist a positively authenticated browser state without exposing a partially-written state
87
+ * file to the next launch. The temporary file stays beside the target so the final rename is
88
+ * atomic on the profile filesystem; cleanup is deliberately best-effort and never masks the
89
+ * storageState/rename error.
90
+ */
91
+ export async function persistWechatSessionState(
92
+ context: { storageState(options: { path: string }): Promise<unknown> },
93
+ profileDir: string,
94
+ ): Promise<void> {
95
+ const target = storageStatePath(profileDir);
96
+ const temporary = `${target}.${process.pid}.${randomUUID()}.tmp`;
97
+ const fs = createNodeFileSystem();
98
+ let reserved = false;
99
+ try {
100
+ createExclusiveFile(temporary);
101
+ reserved = true;
102
+ await context.storageState({ path: temporary });
103
+ chmod(temporary, 0o600);
104
+ await fs.rename(temporary, target);
105
+ } finally {
106
+ if (reserved) {
107
+ await Promise.resolve()
108
+ .then(() => fs.deleteFile(temporary))
109
+ .catch(() => undefined);
110
+ }
111
+ }
112
+ }
113
+
77
114
  /** Headless session preflight: throws (incl. BrowserProfileMissingError) when the profile cannot drive the console.
78
115
  * 0216: `headless: false` probes headed only and `timeoutMs` overrides the ready deadline. */
79
116
  export async function checkWechatSession(
@@ -110,6 +147,8 @@ export async function openWechatLogin(opts: { profileDir: string; timeoutMs?: nu
110
147
  headless: false,
111
148
  launcher: kkBrowserLauncher,
112
149
  });
150
+ let failed = false;
151
+ let failure: unknown;
113
152
  try {
114
153
  const page = context.pages()[0] ?? (await context.newPage());
115
154
  await page.goto(CONSOLE_URL);
@@ -120,10 +159,29 @@ export async function openWechatLogin(opts: { profileDir: string; timeoutMs?: nu
120
159
  );
121
160
  if (!ready) throw new Error(`QR scan not completed within ${timeoutMs / 1000}s`);
122
161
  await page.waitForTimeout(10_000); // settle: let the console finish its post-auth Set-Cookies
123
- await context.storageState({ path: storageStatePath(opts.profileDir) });
124
- } finally {
162
+ try {
163
+ await persistWechatSessionState(context, opts.profileDir);
164
+ } catch {
165
+ // Storage-state errors can contain cookie values. Keep the profile-check surface
166
+ // actionable without relaying an exception that may include session material.
167
+ throw new Error('wechatmp authenticated session snapshot failed');
168
+ }
169
+ } catch (error: unknown) {
170
+ failed = true;
171
+ failure = error;
172
+ }
173
+ try {
125
174
  await context.close();
175
+ } catch {
176
+ // A successful atomic snapshot is already durable; a close failure must not turn it into
177
+ // a retryable login. Keep the diagnostic generic because browser errors may contain session
178
+ // material, and never replace an authentication or snapshot failure with shutdown noise.
179
+ if (!failed)
180
+ echoError(
181
+ 'wechatmp browser context close failed after authenticated session snapshot; saved state preserved',
182
+ );
126
183
  }
184
+ if (failed) throw failure;
127
185
  }
128
186
 
129
187
  /**
@@ -201,7 +259,10 @@ async function openWithState(
201
259
  */
202
260
  async function defaultOpenConsole(profileDir: string): Promise<OpenedConsole> {
203
261
  const { page, context } = await openWithState(profileDir, true);
204
- return wrapPlaywrightContext(context, page);
262
+ return {
263
+ ...wrapPlaywrightContext(context, page),
264
+ persistSession: () => persistWechatSessionState(context, profileDir),
265
+ };
205
266
  }
206
267
 
207
268
  type BrowserDriverName = 'script' | 'agent';
@@ -291,42 +352,63 @@ export class WechatmpBrowserTransport implements PublishTransport {
291
352
  } catch (error: unknown) {
292
353
  return fail(error instanceof Error ? error.message : String(error));
293
354
  }
355
+ let result: Result;
294
356
  try {
295
357
  const ready = await waitUntil(
296
358
  () => Promise.resolve(isConsoleReady(opened.page)),
297
359
  this.consoleReadyTimeoutMs,
298
360
  );
299
361
  if (!ready) {
300
- return fail(
362
+ result = fail(
301
363
  `no authenticated ${TARGET} console within ${this.consoleReadyTimeoutMs}ms — the preflight passed but the console did not report a session route (profile ${profileDir})`,
302
364
  );
303
- }
304
- // The 草稿箱 baseline is read before the editor opens: MP auto-saves continuously, so a
305
- // row that already carried a stamp says nothing about this run's save (discovery §5.1).
306
- const title = typeof content.title === 'string' ? content.title.trim() : '';
307
- const draftBaseline =
308
- publishStatus === 'draft' ? await readDraftBaseline(opened.page, title) : { stamp: null };
365
+ } else {
366
+ // The 草稿箱 baseline is read before the editor opens: MP auto-saves continuously, so a
367
+ // row that already carried a stamp says nothing about this run's save (discovery §5.1).
368
+ const title = typeof content.title === 'string' ? content.title.trim() : '';
369
+ const draftBaseline =
370
+ publishStatus === 'draft' ? await readDraftBaseline(opened.page, title) : { stamp: null };
309
371
 
310
- return await runMode(validation.input, {
311
- entry: opened.page,
312
- pages: opened.pages,
313
- newPage: opened.newPage,
314
- content,
315
- publishStatus,
316
- profileDir,
317
- driver: this.driver,
318
- stepTimeoutMs: this.stepTimeoutMs,
319
- pollMs: this.pollMs,
320
- submitSettleMs: this.submitSettleMs,
321
- draftTimeoutMs: this.draftTimeoutMs,
322
- submitAttempts: this.submitAttempts,
323
- draftBaseline,
324
- });
372
+ result = await runMode(validation.input, {
373
+ entry: opened.page,
374
+ pages: opened.pages,
375
+ newPage: opened.newPage,
376
+ content,
377
+ publishStatus,
378
+ profileDir,
379
+ driver: this.driver,
380
+ stepTimeoutMs: this.stepTimeoutMs,
381
+ pollMs: this.pollMs,
382
+ submitSettleMs: this.submitSettleMs,
383
+ draftTimeoutMs: this.draftTimeoutMs,
384
+ submitAttempts: this.submitAttempts,
385
+ draftBaseline,
386
+ });
387
+ }
325
388
  } catch (error: unknown) {
326
- return fail(error instanceof Error ? error.message : String(error));
389
+ result = fail(error instanceof Error ? error.message : String(error));
327
390
  } finally {
328
- await opened.close();
391
+ if (opened.persistSession !== undefined) {
392
+ try {
393
+ // A positive authenticated route is the only evidence that rotating state is
394
+ // safe to persist. Read-only preflight uses openWithState directly and never
395
+ // receives this callback.
396
+ if (isConsoleReady(opened.page)) await opened.persistSession();
397
+ } catch {
398
+ // Storage-state errors can contain cookie values; the publication result is
399
+ // already determined and must survive this best-effort session refresh.
400
+ echoError('wechatmp-pub: authenticated session snapshot failed; publication result preserved');
401
+ }
402
+ }
403
+ try {
404
+ await opened.close();
405
+ } catch {
406
+ // Context shutdown is not publication evidence. Never turn an already determined
407
+ // Result into a retryable throw, and never relay browser diagnostics verbatim.
408
+ echoError('wechatmp-pub: browser context close failed; publication result preserved');
409
+ }
329
410
  }
411
+ return result;
330
412
  }
331
413
  }
332
414
 
@@ -1,5 +1,6 @@
1
1
  import { trySelectors } from '@gobing-ai/publish-harness';
2
2
  import { waitUntil } from '@gobing-ai/publish-harness/browser';
3
+ import type { Page, Request } from 'playwright';
3
4
 
4
5
  /**
5
6
  * The console's page seam (ADR-029) and the probes every wechatmp operation shares.
@@ -17,6 +18,11 @@ import { waitUntil } from '@gobing-ai/publish-harness/browser';
17
18
  export const POLL_MS = 500;
18
19
  export const STEP_TIMEOUT_MS = 30_000;
19
20
 
21
+ /** Browser evidence may retain navigation context, but never its session query credentials. */
22
+ export function sanitizeSessionEvidence(value: string): string {
23
+ return value.replace(/([?&](?:token|ticket|authkey)=)[^&\s"'<>)]*/gi, '$1[REDACTED]');
24
+ }
25
+
20
26
  /** The playwright-shaped locator surface the flows and the shared helpers read. */
21
27
  export interface WechatLocator {
22
28
  count(): Promise<number>;
@@ -36,6 +42,8 @@ export interface WechatLocator {
36
42
  }
37
43
 
38
44
  export interface WechatPage {
45
+ /** Click the native article save and await its successful, fully received acknowledgement. */
46
+ saveDraftAndWait?(selector: string, timeoutMs: number): Promise<void>;
39
47
  goto(url: string, options?: { timeout?: number }): Promise<void>;
40
48
  locator(selector: string): { first(): WechatLocator; count(): Promise<number> };
41
49
  setInputFiles(selector: string, paths: readonly string[] | string): Promise<void>;
@@ -63,6 +71,8 @@ export interface OpenedConsole {
63
71
  pages(): WechatPage[];
64
72
  /** Opens an extra tab; the token-derived editor fallback uses it so the home stays readable. */
65
73
  newPage(): Promise<WechatPage>;
74
+ /** Best-effort authenticated state snapshot owned by the launcher; absent for read-only seams. */
75
+ persistSession?: () => Promise<void>;
66
76
  close(): Promise<void>;
67
77
  }
68
78
 
@@ -213,9 +223,104 @@ interface RawFrame {
213
223
  locator(selector: string): { count(): Promise<number> };
214
224
  }
215
225
 
226
+ /** Match a new full-article save, excluding reads and other operations at the shared endpoint. */
227
+ function isArticleSave(request: Request, origin: string): boolean {
228
+ if (request.method() !== 'POST') return false;
229
+ const url = new URL(request.url());
230
+ if (url.origin !== origin || url.pathname !== '/cgi-bin/operate_appmsg') return false;
231
+ if (!['create', 'update', 'edit'].includes(url.searchParams.get('sub') ?? '')) return false;
232
+ const data = request.postData() ?? '';
233
+ const form = new URLSearchParams(data);
234
+ return (
235
+ (form.has('content0') && form.has('title0')) ||
236
+ (data.includes('name="content0"') && data.includes('name="title0"'))
237
+ );
238
+ }
239
+
240
+ function saveResponseSucceeded(value: unknown): boolean {
241
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) return false;
242
+ const body = value as Record<string, unknown>;
243
+ const base =
244
+ body.base_resp !== null && typeof body.base_resp === 'object'
245
+ ? (body.base_resp as Record<string, unknown>)
246
+ : {};
247
+ const codes = [body.ret, body.errcode, base.ret].filter((code) => code !== undefined);
248
+ // The live editor returns top-level ret as "0" alongside numeric base_resp.ret.
249
+ if (codes.some((code) => code !== 0 && code !== '0')) return false;
250
+ if (codes.length > 0) return true;
251
+ const info =
252
+ body.appMsgInfo !== null && typeof body.appMsgInfo === 'object'
253
+ ? (body.appMsgInfo as Record<string, unknown>)
254
+ : body;
255
+ return typeof info.appMsgId === 'string' || typeof info.appMsgId === 'number'
256
+ ? /^\d{1,12}$/.test(String(info.appMsgId))
257
+ : false;
258
+ }
259
+
260
+ /** Observe the native request before clicking; neither response headers nor a URL change is completion. */
261
+ async function saveNativeDraft(raw: Page, selector: string, timeoutMs: number): Promise<void> {
262
+ const origin = new URL(raw.url()).origin;
263
+ const marker = `__kkNativeSave_${crypto.randomUUID()}`;
264
+ const button = raw.locator(selector).first();
265
+ await button.evaluate((element, key) => {
266
+ const state = { clickedAt: 0 };
267
+ const handler = () => {
268
+ state.clickedAt = Date.now();
269
+ };
270
+ (globalThis as unknown as Record<string, unknown>)[key] = { state, element, handler };
271
+ element.addEventListener('click', handler, { capture: true, once: true });
272
+ }, marker);
273
+ let deadline: ReturnType<typeof setTimeout> | undefined;
274
+ const responseComplete = raw
275
+ .waitForRequest(
276
+ async (request) => {
277
+ if (!isArticleSave(request, origin)) return false;
278
+ const clickedAt = await raw
279
+ .evaluate(`globalThis[${JSON.stringify(marker)}]?.state?.clickedAt ?? 0`)
280
+ .catch(() => 0);
281
+ if (typeof clickedAt !== 'number' || clickedAt <= 0) return false;
282
+ // Playwright populates request timing when response headers arrive; before that
283
+ // the live Chrome request reports startTime=0. Keep the click stamp across navigation.
284
+ if (!(await request.response())) return false;
285
+ return request.timing().startTime >= clickedAt;
286
+ },
287
+ { timeout: timeoutMs },
288
+ )
289
+ .then(async (request) => {
290
+ const response = await request.response();
291
+ if (response === null || response.status() < 200 || response.status() >= 300)
292
+ throw new Error('HTTP refusal');
293
+ if (await response.finished()) throw new Error('Incomplete response');
294
+ if (!saveResponseSucceeded(await response.json())) throw new Error('Save refused');
295
+ });
296
+ try {
297
+ await Promise.race([
298
+ Promise.all([responseComplete, button.click({ timeout: timeoutMs })]),
299
+ new Promise<never>((_resolve, reject) => {
300
+ deadline = setTimeout(() => reject(new Error('Save timeout')), timeoutMs);
301
+ }),
302
+ ]);
303
+ } catch {
304
+ // Playwright errors and server bodies may contain session URLs; retain no vendor payload.
305
+ throw new Error('Native article save was not acknowledged successfully before the deadline');
306
+ } finally {
307
+ if (deadline !== undefined) clearTimeout(deadline);
308
+ await raw
309
+ .evaluate(`(() => {
310
+ const key = ${JSON.stringify(marker)};
311
+ const records = globalThis;
312
+ const value = records[key];
313
+ if (value?.element && value.handler) value.element.removeEventListener('click', value.handler, true);
314
+ delete records[key];
315
+ })()`)
316
+ .catch(() => undefined);
317
+ }
318
+ }
319
+
216
320
  export function wrapPlaywrightPage(raw: unknown): WechatPage {
217
321
  const pw = raw as PlaywrightLike;
218
322
  return {
323
+ saveDraftAndWait: (selector, timeoutMs) => saveNativeDraft(raw as Page, selector, timeoutMs),
219
324
  goto: (url, options) => pw.goto(url, { waitUntil: 'domcontentloaded', ...options }).then(() => undefined),
220
325
  locator: (selector) => ({
221
326
  count: () => pw.locator(selector).count(),
@@ -20458,9 +20458,11 @@ import { isAbsolute, join as join4, relative, resolve as resolve4, sep } from "p
20458
20458
  // ../../packages/publish-harness/src/bot-fs.ts
20459
20459
  import {
20460
20460
  chmodSync,
20461
+ closeSync,
20461
20462
  copyFileSync,
20462
20463
  lstatSync,
20463
20464
  mkdirSync as mkdirSync2,
20465
+ openSync,
20464
20466
  readdirSync as readdirSync2,
20465
20467
  realpathSync as realpathSync2,
20466
20468
  rmdirSync
@@ -20705,7 +20707,6 @@ class BotError extends Error {
20705
20707
  this.code = code;
20706
20708
  }
20707
20709
  }
20708
-
20709
20710
  // ../../packages/publish-harness/src/profile-dir.ts
20710
20711
  import { mkdir as mkdir2, stat } from "fs/promises";
20711
20712
  async function ensureProfileDir2(profileDir, headless) {
@@ -20119,9 +20119,11 @@ import { isAbsolute, join as join3, relative, resolve as resolve3, sep } from "p
20119
20119
  // ../../packages/publish-harness/src/bot-fs.ts
20120
20120
  import {
20121
20121
  chmodSync,
20122
+ closeSync,
20122
20123
  copyFileSync,
20123
20124
  lstatSync,
20124
20125
  mkdirSync as mkdirSync2,
20126
+ openSync,
20125
20127
  readdirSync as readdirSync2,
20126
20128
  realpathSync as realpathSync2,
20127
20129
  rmdirSync
@@ -20373,7 +20375,6 @@ class BotError extends Error {
20373
20375
  this.code = code;
20374
20376
  }
20375
20377
  }
20376
-
20377
20378
  // ../../packages/publish-harness/src/index.ts
20378
20379
  import { homedir as homedir4 } from "os";
20379
20380
  import { join as join4, resolve as resolve4 } from "path";
@@ -20483,9 +20483,11 @@ import { isAbsolute, join as join4, relative, resolve as resolve4, sep } from "p
20483
20483
  // ../../packages/publish-harness/src/bot-fs.ts
20484
20484
  import {
20485
20485
  chmodSync,
20486
+ closeSync,
20486
20487
  copyFileSync,
20487
20488
  lstatSync,
20488
20489
  mkdirSync as mkdirSync2,
20490
+ openSync,
20489
20491
  readdirSync as readdirSync2,
20490
20492
  realpathSync as realpathSync2,
20491
20493
  rmdirSync
@@ -20730,7 +20732,6 @@ class BotError extends Error {
20730
20732
  this.code = code;
20731
20733
  }
20732
20734
  }
20733
-
20734
20735
  // ../../packages/publish-harness/src/profile-dir.ts
20735
20736
  import { mkdir as mkdir2, stat } from "fs/promises";
20736
20737
  async function ensureProfileDir2(profileDir, headless) {
@@ -21007,6 +21008,13 @@ function resolveOperation(options, content) {
21007
21008
  class XPostApiTransport {
21008
21009
  kind = "file-cli";
21009
21010
  async publish(payload) {
21011
+ const requestedStatus = payload.content.metadata?.publishStatus;
21012
+ if (requestedStatus === "draft") {
21013
+ return fail("x post has no draft: the post API publishes immediately \u2014 plan this target dummy/off or launch it live");
21014
+ }
21015
+ if (requestedStatus !== undefined && requestedStatus !== "public") {
21016
+ return fail(`unknown publishStatus '${String(requestedStatus)}' (expected absent, 'draft' or 'public')`);
21017
+ }
21010
21018
  const missing = TOKEN_ENVS.filter((key) => {
21011
21019
  const value = process.env[key];
21012
21020
  return !value || value.trim().length === 0;
@@ -20119,9 +20119,11 @@ import { isAbsolute, join as join3, relative, resolve as resolve3, sep } from "p
20119
20119
  // ../../packages/publish-harness/src/bot-fs.ts
20120
20120
  import {
20121
20121
  chmodSync,
20122
+ closeSync,
20122
20123
  copyFileSync,
20123
20124
  lstatSync,
20124
20125
  mkdirSync as mkdirSync2,
20126
+ openSync,
20125
20127
  readdirSync as readdirSync2,
20126
20128
  realpathSync as realpathSync2,
20127
20129
  rmdirSync
@@ -20373,7 +20375,6 @@ class BotError extends Error {
20373
20375
  this.code = code;
20374
20376
  }
20375
20377
  }
20376
-
20377
20378
  // ../../packages/publish-harness/src/index.ts
20378
20379
  import { homedir as homedir4 } from "os";
20379
20380
  import { join as join4, resolve as resolve4 } from "path";
@@ -105,6 +105,17 @@ export class XPostApiTransport implements PublishTransport {
105
105
  public readonly kind = 'file-cli';
106
106
 
107
107
  public async publish(payload: PublishTransportPayload): Promise<Result> {
108
+ // 0330 R2: the post API publishes immediately — there is no draft. Intercept before the
109
+ // credential lookup so a planned draft never touches X, and fail closed on unknown modes.
110
+ const requestedStatus = payload.content.metadata?.publishStatus;
111
+ if (requestedStatus === 'draft') {
112
+ return fail(
113
+ 'x post has no draft: the post API publishes immediately — plan this target dummy/off or launch it live',
114
+ );
115
+ }
116
+ if (requestedStatus !== undefined && requestedStatus !== 'public') {
117
+ return fail(`unknown publishStatus '${String(requestedStatus)}' (expected absent, 'draft' or 'public')`);
118
+ }
108
119
  const missing = TOKEN_ENVS.filter((key) => {
109
120
  const value = process.env[key];
110
121
  return !value || value.trim().length === 0;
@@ -20458,9 +20458,11 @@ import { isAbsolute, join as join4, relative, resolve as resolve4, sep } from "p
20458
20458
  // ../../packages/publish-harness/src/bot-fs.ts
20459
20459
  import {
20460
20460
  chmodSync,
20461
+ closeSync,
20461
20462
  copyFileSync,
20462
20463
  lstatSync,
20463
20464
  mkdirSync as mkdirSync2,
20465
+ openSync,
20464
20466
  readdirSync as readdirSync2,
20465
20467
  realpathSync as realpathSync2,
20466
20468
  rmdirSync
@@ -20705,7 +20707,6 @@ class BotError extends Error {
20705
20707
  this.code = code;
20706
20708
  }
20707
20709
  }
20708
-
20709
20710
  // ../../packages/publish-harness/src/profile-dir.ts
20710
20711
  import { mkdir as mkdir2, stat } from "fs/promises";
20711
20712
  async function ensureProfileDir2(profileDir, headless) {
@@ -20127,9 +20127,11 @@ import { isAbsolute, join as join3, relative, resolve as resolve3, sep } from "p
20127
20127
  // ../../packages/publish-harness/src/bot-fs.ts
20128
20128
  import {
20129
20129
  chmodSync,
20130
+ closeSync,
20130
20131
  copyFileSync,
20131
20132
  lstatSync,
20132
20133
  mkdirSync as mkdirSync2,
20134
+ openSync,
20133
20135
  readdirSync as readdirSync2,
20134
20136
  realpathSync as realpathSync2,
20135
20137
  rmdirSync
@@ -20374,7 +20376,6 @@ class BotError extends Error {
20374
20376
  this.code = code;
20375
20377
  }
20376
20378
  }
20377
-
20378
20379
  // ../../packages/publish-harness/src/index.ts
20379
20380
  import { homedir as homedir4 } from "os";
20380
20381
  import { join as join4, resolve as resolve4 } from "path";
@@ -20464,9 +20464,11 @@ import { isAbsolute, join as join4, relative, resolve as resolve4, sep } from "p
20464
20464
  // ../../packages/publish-harness/src/bot-fs.ts
20465
20465
  import {
20466
20466
  chmodSync,
20467
+ closeSync,
20467
20468
  copyFileSync,
20468
20469
  lstatSync,
20469
20470
  mkdirSync as mkdirSync2,
20471
+ openSync,
20470
20472
  readdirSync as readdirSync2,
20471
20473
  realpathSync as realpathSync2,
20472
20474
  rmdirSync
@@ -20711,7 +20713,6 @@ class BotError extends Error {
20711
20713
  this.code = code;
20712
20714
  }
20713
20715
  }
20714
-
20715
20716
  // ../../packages/publish-harness/src/profile-dir.ts
20716
20717
  import { mkdir as mkdir2, stat } from "fs/promises";
20717
20718
  async function ensureProfileDir2(profileDir, headless) {
@@ -19847,9 +19847,11 @@ import { isAbsolute, join as join3, relative, resolve as resolve3, sep } from "p
19847
19847
  // ../../packages/publish-harness/src/bot-fs.ts
19848
19848
  import {
19849
19849
  chmodSync,
19850
+ closeSync,
19850
19851
  copyFileSync,
19851
19852
  lstatSync,
19852
19853
  mkdirSync as mkdirSync2,
19854
+ openSync,
19853
19855
  readdirSync as readdirSync2,
19854
19856
  realpathSync as realpathSync2,
19855
19857
  rmdirSync
@@ -20101,7 +20103,6 @@ class BotError extends Error {
20101
20103
  this.code = code;
20102
20104
  }
20103
20105
  }
20104
-
20105
20106
  // ../../packages/publish-harness/src/index.ts
20106
20107
  import { homedir as homedir4 } from "os";
20107
20108
  import { join as join4, resolve as resolve4 } from "path";