@gobing-ai/knowledge-kit 0.1.11 → 0.1.13

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 (137) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/dist/index.js +2372 -1722
  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 +15 -7
  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 +91 -41
  88. package/plugins/kk/scripts/daily-stages.ts +394 -53
  89. package/plugins/kk/scripts/kk-workflow-stages.ts +190 -0
  90. package/plugins/kk/scripts/safety-artifact-check.ts +70 -28
  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/content-safety/references/platforms/podcast-salty.md +41 -0
  97. package/plugins/kk/skills/content-safety/references/platforms/surfdash.md +36 -0
  98. package/plugins/kk/skills/content-safety/references/platforms/xhs.md +39 -0
  99. package/plugins/kk/skills/explain-things/SKILL.md +1 -0
  100. package/plugins/kk/skills/image-authoring/SKILL.md +1 -0
  101. package/plugins/kk/skills/itc-generating/SKILL.md +1 -0
  102. package/plugins/kk/skills/publish/SKILL.md +1 -0
  103. package/plugins/kk/skills/smart-seo/SKILL.md +1 -0
  104. package/plugins/kk/skills/storm-research/SKILL.md +1 -0
  105. package/plugins/kk/skills/taste-unslop/SKILL.md +1 -0
  106. package/plugins/kk/skills/topic/SKILL.md +1 -0
  107. package/plugins/kk/skills/web-automation/SKILL.md +1 -0
  108. package/plugins/kk/skills/xhs-images/SKILL.md +1 -0
  109. package/plugins/kk/workflows/kk-collect.yaml +3 -3
  110. package/plugins/kk/workflows/kk-daily-ai-voice2.yaml +21 -19
  111. package/plugins/kk/workflows/kk-daily-ai-voice3.yaml +163 -63
  112. package/plugins/kk/workflows/kk-daily-publish3.yaml +82 -26
  113. package/plugins/kk/workflows/kk-daily-safety.yaml +39 -31
  114. package/plugins/kk/workflows/kk-itc-author.yaml +75 -0
  115. package/plugins/kk/workflows/kk-itc-publish-tail.yaml +25 -3
  116. package/plugins/kk/workflows/kk-itc.yaml +36 -4
  117. package/plugins/publishings/linkedin-pub/dist/index.js +21333 -0
  118. package/plugins/publishings/linkedin-pub/dist/profile-check.js +21005 -0
  119. package/plugins/publishings/linkedin-pub/package.json +19 -0
  120. package/plugins/publishings/linkedin-pub/plugin.json +12 -0
  121. package/plugins/publishings/linkedin-pub/src/index.ts +705 -0
  122. package/plugins/publishings/linkedin-pub/src/profile-check.ts +17 -0
  123. package/plugins/publishings/linkedin-pub/tsconfig.json +4 -0
  124. package/plugins/publishings/surfdash-pub/dist/index.js +53 -24
  125. package/plugins/publishings/surfdash-pub/src/index.ts +72 -25
  126. package/plugins/publishings/threads-pub/dist/index.js +7 -0
  127. package/plugins/publishings/threads-pub/src/index.ts +12 -0
  128. package/plugins/publishings/wechatmp-pub/dist/index.js +176 -32
  129. package/plugins/publishings/wechatmp-pub/src/flow.ts +298 -52
  130. package/plugins/publishings/weibo-pub/dist/index.js +100 -7
  131. package/plugins/publishings/weibo-pub/src/index.ts +111 -11
  132. package/plugins/publishings/x-pub/dist/index.js +7 -0
  133. package/plugins/publishings/x-pub/src/index.ts +11 -0
  134. package/plugins/publishings/xhs-pub/dist/index.js +164 -12
  135. package/plugins/publishings/xhs-pub/dist/profile-check.js +11 -1
  136. package/plugins/publishings/xhs-pub/src/flow.ts +194 -23
  137. package/plugins/publishings/xhs-pub/src/page.ts +26 -0
@@ -20991,7 +20991,7 @@ function normalizeText(value) {
20991
20991
  return value.replace(/\s+/g, "");
20992
20992
  }
20993
20993
  function excerpt(text) {
20994
- return normalizeText(text).slice(0, EXCERPT_CHARS);
20994
+ return normalizeText(text.replace(/^[#>*`\-\s]+/gm, "")).slice(0, EXCERPT_CHARS);
20995
20995
  }
20996
20996
  function resolveOperation(options, content) {
20997
20997
  const raw = options?.operation ?? content?.metadata?.operation ?? "post";
@@ -21118,6 +21118,9 @@ class WeiboBrowserTransport {
21118
21118
  async runArticleFlow(page, content, publishStatus, coverPath, profileDir) {
21119
21119
  const metadata = { operation: "article", driver: this.driver, publishStatus, profileDir };
21120
21120
  const title = content.title ?? "Untitled";
21121
+ await page.evaluate(ensureArticleDraftScript());
21122
+ const editorUrl = page.url();
21123
+ const toastsBefore = await this.readToasts(page);
21121
21124
  const titleSelector = await resolveSelector(page, WEIBO_ARTICLE_SELECTORS.title, {
21122
21125
  timeoutMs: this.stepTimeoutMs,
21123
21126
  label: "article title input"
@@ -21129,12 +21132,19 @@ class WeiboBrowserTransport {
21129
21132
  });
21130
21133
  await pasteHtml(page, bodySelector, markdownToHtml(content.body), { timeoutMs: this.stepTimeoutMs });
21131
21134
  if (coverPath !== undefined) {
21135
+ const baseline = await this.countImages(page, bodySelector);
21136
+ const preAttached = (await trySelectors(page, WEIBO_ARTICLE_SELECTORS.fileInput, { visible: false })).found;
21137
+ if (!preAttached) {
21138
+ await page.evaluate(openCoverLibraryScript());
21139
+ }
21132
21140
  const fileInputSelector = await resolveSelector(page, WEIBO_ARTICLE_SELECTORS.fileInput, {
21133
21141
  timeoutMs: this.stepTimeoutMs,
21134
21142
  label: "article cover file input"
21135
21143
  });
21136
- const baseline = await this.countImages(page, bodySelector);
21137
21144
  await uploadFile(page, fileInputSelector, coverPath, { timeoutMs: this.stepTimeoutMs });
21145
+ if (!preAttached) {
21146
+ await page.evaluate(confirmCoverModalScript());
21147
+ }
21138
21148
  const attached = await expectOutcome(() => this.probeCoverAttached(page, bodySelector, baseline), {
21139
21149
  timeoutMs: this.stepTimeoutMs,
21140
21150
  label: "weibo article cover attachment"
@@ -21148,8 +21158,6 @@ class WeiboBrowserTransport {
21148
21158
  const bodyReadback = await this.requireReadback(page, bodySelector, content.body, "article body readback");
21149
21159
  if (!bodyReadback.ok)
21150
21160
  return fail(bodyReadback.error);
21151
- const editorUrl = page.url();
21152
- const toastsBefore = await this.readToasts(page);
21153
21161
  if (publishStatus === "draft") {
21154
21162
  const saveSelector = await resolveSelector(page, WEIBO_ARTICLE_SELECTORS.saveDraft, {
21155
21163
  timeoutMs: this.stepTimeoutMs,
@@ -21277,7 +21285,10 @@ class WeiboBrowserTransport {
21277
21285
  async probeFreshToast(page, toastsBefore) {
21278
21286
  const before = new Set(toastsBefore);
21279
21287
  const now = await this.readToasts(page);
21280
- return now.find((text) => !before.has(text) && /\u4FDD\u5B58|\u8349\u7A3F|\u6210\u529F/.test(text)) ?? null;
21288
+ const hit = now.find((text) => !before.has(text) && /\u4FDD\u5B58|\u8349\u7A3F|\u6210\u529F|\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}/.test(text));
21289
+ if (hit === undefined)
21290
+ return null;
21291
+ return /#\/draft\/\d+/.test(page.url()) ? page.url() : hit;
21281
21292
  }
21282
21293
  async probeArticleLive(page) {
21283
21294
  return liveArticleUrl(page.url());
@@ -21400,12 +21411,94 @@ function coverCountScript(selector) {
21400
21411
  return `(() => { // kk-weibo-cover
21401
21412
  const anchor = document.querySelector(${JSON.stringify(selector)});
21402
21413
  const scope = anchor === null ? document.body : anchor.closest('form, section, article, main') || document.body;
21403
- return String(scope.querySelectorAll('img[src]').length);
21414
+ return String(Array.from(scope.querySelectorAll('img[src]')).filter((el) => !el.closest('.n-modal')).length);
21415
+ })()`;
21416
+ }
21417
+ function ensureArticleDraftScript() {
21418
+ return `(async () => { // kk-weibo-draft-init
21419
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
21420
+ const spin = () => document.querySelectorAll('.wb-editor-spin').length > 0;
21421
+ const titleEl = document.querySelector('input[placeholder*="\u6807\u9898"], textarea[placeholder*="\u6807\u9898"]');
21422
+ const titleVal = titleEl && titleEl.value ? String(titleEl.value).trim() : '';
21423
+ const hasCover = document.querySelectorAll('.cover-preview, img.cover-img').length > 0;
21424
+ if (spin() || titleVal.length > 0 || hasCover) {
21425
+ for (const btn of document.querySelectorAll('button')) {
21426
+ const t = (btn.textContent || '').trim();
21427
+ if (t === '\u5199\u6587\u7AE0' || t.includes('\u521B\u4F5C\u4E00\u7BC7\u65B0\u6587\u7AE0')) {
21428
+ btn.click();
21429
+ break;
21430
+ }
21431
+ }
21432
+ await sleep(600);
21433
+ for (let i = 0; i < 15 && spin(); i += 1) await sleep(200);
21434
+ }
21435
+ return true;
21436
+ })()`;
21437
+ }
21438
+ function openCoverLibraryScript() {
21439
+ return `(async () => { // kk-weibo-open-cover
21440
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
21441
+ let coverEmpty = null;
21442
+ for (let i = 0; i < 15; i += 1) {
21443
+ coverEmpty = document.querySelector('div.cover-empty, [class*="cover-empty"]');
21444
+ if (coverEmpty) break;
21445
+ await sleep(200);
21446
+ }
21447
+ if (coverEmpty instanceof HTMLElement) coverEmpty.click();
21448
+ for (let i = 0; i < 15 && !document.querySelector('.n-modal'); i += 1) await sleep(200);
21449
+ for (const tab of document.querySelectorAll('.n-modal .n-tabs-tab')) {
21450
+ if ((tab.textContent || '').trim() === '\u56FE\u7247\u5E93' && tab instanceof HTMLElement) {
21451
+ tab.click();
21452
+ }
21453
+ }
21454
+ for (let i = 0; i < 15 && !document.querySelector('input[type="file"]'); i += 1) await sleep(200);
21455
+ return true;
21456
+ })()`;
21457
+ }
21458
+ function confirmCoverModalScript() {
21459
+ return `(async () => { // kk-weibo-confirm-cover
21460
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
21461
+ if (!document.querySelector('.n-modal')) return true;
21462
+ await sleep(1500);
21463
+ let item = null;
21464
+ for (let i = 0; i < 25; i += 1) {
21465
+ item = document.querySelector('.n-modal .image-item, .n-modal img[src]');
21466
+ if (item) break;
21467
+ await sleep(200);
21468
+ }
21469
+ if (item instanceof HTMLElement && !item.classList.contains('is-selected')) {
21470
+ item.click();
21471
+ }
21472
+ for (let i = 0; i < 15; i += 1) {
21473
+ const nextBtn = Array.from(document.querySelectorAll('.n-modal button')).find(
21474
+ (b) => (b.textContent || '').trim() === '\u4E0B\u4E00\u6B65' && !b.disabled,
21475
+ );
21476
+ if (nextBtn) {
21477
+ nextBtn.click();
21478
+ break;
21479
+ }
21480
+ await sleep(200);
21481
+ }
21482
+ await sleep(1800);
21483
+ for (let i = 0; i < 15; i += 1) {
21484
+ const okBtn = Array.from(document.querySelectorAll('.n-modal button')).find(
21485
+ (b) => (b.textContent || '').trim() === '\u786E\u5B9A' && !b.disabled,
21486
+ );
21487
+ if (okBtn) {
21488
+ okBtn.click();
21489
+ break;
21490
+ }
21491
+ await sleep(200);
21492
+ }
21493
+ for (let i = 0; i < 25 && document.querySelector('.n-modal'); i += 1) {
21494
+ await sleep(200);
21495
+ }
21496
+ return true;
21404
21497
  })()`;
21405
21498
  }
21406
21499
  function toastTextsScript() {
21407
21500
  return `(() => { // kk-weibo-toasts
21408
- const nodes = document.querySelectorAll('[class*="toast" i], [class*="Toast"], [class*="message" i], [role="alert"]');
21501
+ const nodes = document.querySelectorAll('[class*="toast" i], [class*="Toast"], [class*="message" i], [role="alert"], .list-item');
21409
21502
  const texts = [];
21410
21503
  for (const node of nodes) {
21411
21504
  const text = (node.textContent || '').trim();
@@ -143,7 +143,7 @@ function normalizeText(value: string): string {
143
143
  }
144
144
 
145
145
  function excerpt(text: string): string {
146
- return normalizeText(text).slice(0, EXCERPT_CHARS);
146
+ return normalizeText(text.replace(/^[#>*`\-\s]+/gm, '')).slice(0, EXCERPT_CHARS);
147
147
  }
148
148
 
149
149
  /**
@@ -368,6 +368,10 @@ export class WeiboBrowserTransport implements PublishTransport {
368
368
  const metadata = { operation: 'article', driver: this.driver, publishStatus, profileDir };
369
369
  const title = content.title ?? 'Untitled';
370
370
 
371
+ await page.evaluate(ensureArticleDraftScript());
372
+ const editorUrl = page.url();
373
+ const toastsBefore = await this.readToasts(page);
374
+
371
375
  const titleSelector = await resolveSelector(page, WEIBO_ARTICLE_SELECTORS.title, {
372
376
  timeoutMs: this.stepTimeoutMs,
373
377
  label: 'article title input',
@@ -382,12 +386,19 @@ export class WeiboBrowserTransport implements PublishTransport {
382
386
  await pasteHtml(page, bodySelector, markdownToHtml(content.body), { timeoutMs: this.stepTimeoutMs });
383
387
 
384
388
  if (coverPath !== undefined) {
389
+ const baseline = await this.countImages(page, bodySelector);
390
+ const preAttached = (await trySelectors(page, WEIBO_ARTICLE_SELECTORS.fileInput, { visible: false })).found;
391
+ if (!preAttached) {
392
+ await page.evaluate(openCoverLibraryScript());
393
+ }
385
394
  const fileInputSelector = await resolveSelector(page, WEIBO_ARTICLE_SELECTORS.fileInput, {
386
395
  timeoutMs: this.stepTimeoutMs,
387
396
  label: 'article cover file input',
388
397
  });
389
- const baseline = await this.countImages(page, bodySelector);
390
398
  await uploadFile(page, fileInputSelector, coverPath, { timeoutMs: this.stepTimeoutMs });
399
+ if (!preAttached) {
400
+ await page.evaluate(confirmCoverModalScript());
401
+ }
391
402
  // A requested cover needs a fresh observed attachment, not a successful upload call.
392
403
  const attached = await expectOutcome(() => this.probeCoverAttached(page, bodySelector, baseline), {
393
404
  timeoutMs: this.stepTimeoutMs,
@@ -403,9 +414,6 @@ export class WeiboBrowserTransport implements PublishTransport {
403
414
  const bodyReadback = await this.requireReadback(page, bodySelector, content.body, 'article body readback');
404
415
  if (!bodyReadback.ok) return fail(bodyReadback.error);
405
416
 
406
- const editorUrl = page.url();
407
- const toastsBefore = await this.readToasts(page);
408
-
409
417
  if (publishStatus === 'draft') {
410
418
  const saveSelector = await resolveSelector(page, WEIBO_ARTICLE_SELECTORS.saveDraft, {
411
419
  timeoutMs: this.stepTimeoutMs,
@@ -590,11 +598,15 @@ export class WeiboBrowserTransport implements PublishTransport {
590
598
  return parts.join('; ');
591
599
  }
592
600
 
593
- /** A toast carrying a save/success word that was not already on screen before the click. */
601
+ /** A toast or v5 draft-list item carrying a save/success/timestamp signal not on screen before the flow. */
594
602
  private async probeFreshToast(page: Page, toastsBefore: readonly string[]): Promise<string | null> {
595
603
  const before = new Set(toastsBefore);
596
604
  const now = await this.readToasts(page);
597
- return now.find((text) => !before.has(text) && /保存|草稿|成功/.test(text)) ?? null;
605
+ const hit = now.find(
606
+ (text) => !before.has(text) && /保存|草稿|成功|\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}/.test(text),
607
+ );
608
+ if (hit === undefined) return null;
609
+ return /#\/draft\/\d+/.test(page.url()) ? page.url() : hit;
598
610
  }
599
611
 
600
612
  /** Live article evidence: the browser left the editor for a published article URL. */
@@ -751,19 +763,107 @@ function readbackScript(selector: string): string {
751
763
  })()`;
752
764
  }
753
765
 
754
- /** Counts rendered images in the editor's nearest container, for the cover baseline. */
766
+ /** Counts rendered images in the editor's nearest container (excluding modal portals), for the cover baseline. */
755
767
  function coverCountScript(selector: string): string {
756
768
  return `(() => { // kk-weibo-cover
757
769
  const anchor = document.querySelector(${JSON.stringify(selector)});
758
770
  const scope = anchor === null ? document.body : anchor.closest('form, section, article, main') || document.body;
759
- return String(scope.querySelectorAll('img[src]').length);
771
+ return String(Array.from(scope.querySelectorAll('img[src]')).filter((el) => !el.closest('.n-modal')).length);
772
+ })()`;
773
+ }
774
+
775
+ /** Ensures the v5 article editor has an active, blank draft pane before filling title/body. */
776
+ function ensureArticleDraftScript(): string {
777
+ return `(async () => { // kk-weibo-draft-init
778
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
779
+ const spin = () => document.querySelectorAll('.wb-editor-spin').length > 0;
780
+ const titleEl = document.querySelector('input[placeholder*="标题"], textarea[placeholder*="标题"]');
781
+ const titleVal = titleEl && titleEl.value ? String(titleEl.value).trim() : '';
782
+ const hasCover = document.querySelectorAll('.cover-preview, img.cover-img').length > 0;
783
+ if (spin() || titleVal.length > 0 || hasCover) {
784
+ for (const btn of document.querySelectorAll('button')) {
785
+ const t = (btn.textContent || '').trim();
786
+ if (t === '写文章' || t.includes('创作一篇新文章')) {
787
+ btn.click();
788
+ break;
789
+ }
790
+ }
791
+ await sleep(600);
792
+ for (let i = 0; i < 15 && spin(); i += 1) await sleep(200);
793
+ }
794
+ return true;
795
+ })()`;
796
+ }
797
+
798
+ /** Opens the v5 cover modal (`div.cover-empty`) and switches to the `图片库` tab so `input[type="file"]` mounts. */
799
+ function openCoverLibraryScript(): string {
800
+ return `(async () => { // kk-weibo-open-cover
801
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
802
+ let coverEmpty = null;
803
+ for (let i = 0; i < 15; i += 1) {
804
+ coverEmpty = document.querySelector('div.cover-empty, [class*="cover-empty"]');
805
+ if (coverEmpty) break;
806
+ await sleep(200);
807
+ }
808
+ if (coverEmpty instanceof HTMLElement) coverEmpty.click();
809
+ for (let i = 0; i < 15 && !document.querySelector('.n-modal'); i += 1) await sleep(200);
810
+ for (const tab of document.querySelectorAll('.n-modal .n-tabs-tab')) {
811
+ if ((tab.textContent || '').trim() === '图片库' && tab instanceof HTMLElement) {
812
+ tab.click();
813
+ }
814
+ }
815
+ for (let i = 0; i < 15 && !document.querySelector('input[type="file"]'); i += 1) await sleep(200);
816
+ return true;
817
+ })()`;
818
+ }
819
+
820
+ /** Selects the uploaded image in the v5 `.n-modal` library, advances through crop (`下一步` → `确定`), and waits for the modal to close. */
821
+ function confirmCoverModalScript(): string {
822
+ return `(async () => { // kk-weibo-confirm-cover
823
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
824
+ if (!document.querySelector('.n-modal')) return true;
825
+ await sleep(1500);
826
+ let item = null;
827
+ for (let i = 0; i < 25; i += 1) {
828
+ item = document.querySelector('.n-modal .image-item, .n-modal img[src]');
829
+ if (item) break;
830
+ await sleep(200);
831
+ }
832
+ if (item instanceof HTMLElement && !item.classList.contains('is-selected')) {
833
+ item.click();
834
+ }
835
+ for (let i = 0; i < 15; i += 1) {
836
+ const nextBtn = Array.from(document.querySelectorAll('.n-modal button')).find(
837
+ (b) => (b.textContent || '').trim() === '下一步' && !b.disabled,
838
+ );
839
+ if (nextBtn) {
840
+ nextBtn.click();
841
+ break;
842
+ }
843
+ await sleep(200);
844
+ }
845
+ await sleep(1800);
846
+ for (let i = 0; i < 15; i += 1) {
847
+ const okBtn = Array.from(document.querySelectorAll('.n-modal button')).find(
848
+ (b) => (b.textContent || '').trim() === '确定' && !b.disabled,
849
+ );
850
+ if (okBtn) {
851
+ okBtn.click();
852
+ break;
853
+ }
854
+ await sleep(200);
855
+ }
856
+ for (let i = 0; i < 25 && document.querySelector('.n-modal'); i += 1) {
857
+ await sleep(200);
858
+ }
859
+ return true;
760
860
  })()`;
761
861
  }
762
862
 
763
- /** Visible toast texts, used as the pre-save baseline and as the fresh-signal read. */
863
+ /** Visible toast texts and v5 draft sidebar items, used as the pre-save baseline and as the fresh-signal read. */
764
864
  function toastTextsScript(): string {
765
865
  return `(() => { // kk-weibo-toasts
766
- const nodes = document.querySelectorAll('[class*="toast" i], [class*="Toast"], [class*="message" i], [role="alert"]');
866
+ const nodes = document.querySelectorAll('[class*="toast" i], [class*="Toast"], [class*="message" i], [role="alert"], .list-item');
767
867
  const texts = [];
768
868
  for (const node of nodes) {
769
869
  const text = (node.textContent || '').trim();
@@ -21008,6 +21008,13 @@ function resolveOperation(options, content) {
21008
21008
  class XPostApiTransport {
21009
21009
  kind = "file-cli";
21010
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
+ }
21011
21018
  const missing = TOKEN_ENVS.filter((key) => {
21012
21019
  const value = process.env[key];
21013
21020
  return !value || value.trim().length === 0;
@@ -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;
@@ -21342,6 +21342,7 @@ function wrapPlaywrightPage(raw, shadow) {
21342
21342
  isVisible: (options) => loc.isVisible(options),
21343
21343
  fill: (text, options) => loc.fill(text, options),
21344
21344
  click: (options) => loc.click(options),
21345
+ boundingBox: async () => (await onScreen(selector)).at(-1)?.box ?? await loc.boundingBox?.() ?? null,
21345
21346
  type: (text, options) => loc.pressSequentially(text, options),
21346
21347
  textContent: () => loc.textContent()
21347
21348
  };
@@ -21360,7 +21361,16 @@ function wrapPlaywrightPage(raw, shadow) {
21360
21361
  return true;
21361
21362
  },
21362
21363
  findShadowControl: async (labels) => shadow ? shadow.findControl(labels) : null,
21363
- clickAt: (x, y) => pw.mouse.click(x, y)
21364
+ clickAt: (x, y) => pw.mouse.click(x, y),
21365
+ waitForEvent: (event, options) => {
21366
+ if (typeof pw.waitForEvent !== "function") {
21367
+ return Promise.reject(new Error(`the page seam does not expose a '${event}' event source`));
21368
+ }
21369
+ return pw.waitForEvent(event, options);
21370
+ },
21371
+ mouse: {
21372
+ click: (x, y) => pw.mouse.click(x, y)
21373
+ }
21364
21374
  };
21365
21375
  }
21366
21376
  async function findSelector(page, selectors, what, timeoutMs = STEP_TIMEOUT_MS) {
@@ -21412,8 +21422,9 @@ var ARTICLE_LAYOUT = ['button:has-text("\u4E00\u952E\u6392\u7248")', "div.ailayo
21412
21422
  var PODCAST_OPEN = ["text=\u4E0A\u4F20\u97F3\u9891", 'button:has-text("\u4E0A\u4F20\u97F3\u9891")'];
21413
21423
  var PODCAST_MODAL = ["div.creator-modal-sty", ".d-modal.d-modal-centered", ".d-modal"];
21414
21424
  var PODCAST_AUDIO_INPUT = ['input[type="file"][accept*=".mp3"]', 'input[type="file"][accept*=".wav"]'];
21415
- var PODCAST_COVER_INPUT = ['input[type="file"][accept*=".jpg"]', 'input[type="file"][accept*=".png"]'];
21416
21425
  var PODCAST_MODAL_ADVANCE = ["\u53BB\u53D1\u5E03"];
21426
+ var PODCAST_COVER_ZONE = '.d-modal div:has-text("\u8BBE\u7F6E\u5C01\u9762")';
21427
+ var PODCAST_UPLOAD_ACK = "\u4E0A\u4F20\u6210\u529F";
21417
21428
  var DRAFT_LABELS = ["\u6682\u5B58\u79BB\u5F00", "\u5B58\u8349\u7A3F", "\u4FDD\u5B58\u8349\u7A3F"];
21418
21429
  var PUBLISH_LABELS = ["\u53D1\u5E03"];
21419
21430
  var PUBLISH_CONFIRM = ["\u53D1\u5E03\u6210\u529F", "\u5DF2\u53D1\u5E03"];
@@ -21589,18 +21600,67 @@ async function composeArticle(deps) {
21589
21600
  async function isPodcastModalOpen(page) {
21590
21601
  return trySelectors(page, PODCAST_MODAL).then((found) => found.found).catch(() => false);
21591
21602
  }
21603
+ async function clickPodcastModalDomFallback(page) {
21604
+ const res = await page.evaluate(`(() => {
21605
+ const modals = Array.from(document.querySelectorAll('.d-modal, div.creator-modal-sty'));
21606
+ const buttons = Array.from(document.querySelectorAll('.d-modal button, div.creator-modal-sty button, .d-modal-mask button'));
21607
+ const confirmLabels = ['\u786E\u5B9A', '\u5B8C\u6210', '\u786E\u8BA4', '\u4FDD\u5B58'];
21608
+ const cropBtn = buttons.find((b) => {
21609
+ const t = (b.textContent || '').trim();
21610
+ return confirmLabels.includes(t) && !b.classList.contains('cancel-btn') && !b.dataset.kkClicked;
21611
+ }) || (modals.length > 1 ? Array.from(modals[modals.length - 1].querySelectorAll('button')).find((b) => !b.classList.contains('cancel-btn') && b.classList.contains('bg-red') && !(b.textContent || '').includes('\u53BB\u53D1\u5E03') && !b.dataset.kkClicked) : null);
21612
+ if (cropBtn && typeof cropBtn.click === 'function') {
21613
+ cropBtn.dataset.kkClicked = '1';
21614
+ cropBtn.click();
21615
+ return JSON.stringify({ modals: modals.length, clickedCrop: true, clickedAdvance: false });
21616
+ }
21617
+ const advBtns = buttons.filter((b) => (b.textContent || '').includes('\u53BB\u53D1\u5E03') && !b.hasAttribute('disabled') && b.getAttribute('aria-disabled') !== 'true');
21618
+ const adv = advBtns[advBtns.length - 1];
21619
+ let clickedAdvance = false;
21620
+ if (adv && !adv.dataset.kkClicked && typeof adv.click === 'function') {
21621
+ adv.dataset.kkClicked = '1';
21622
+ adv.click();
21623
+ clickedAdvance = true;
21624
+ }
21625
+ return JSON.stringify({
21626
+ modals: modals.length,
21627
+ clickedCrop: false,
21628
+ clickedAdvance,
21629
+ buttons: buttons.map((b) => ({ text: (b.textContent || '').trim(), cls: b.className, disabled: b.hasAttribute('disabled') })),
21630
+ });
21631
+ })()`).catch(() => "");
21632
+ return typeof res === "string" ? res : "";
21633
+ }
21592
21634
  async function leavePodcastModal(deps) {
21593
21635
  const closed = () => isPodcastModalOpen(deps.page).then((open) => !open);
21594
- if (await waitUntil(closed, deps.stepTimeoutMs))
21636
+ if (!await isPodcastModalOpen(deps.page))
21637
+ return;
21638
+ let usedFallback = false;
21639
+ let lastDiag = "";
21640
+ try {
21641
+ const advance = await clickLightControl(deps.page, PODCAST_MODAL_ADVANCE, Math.min(deps.stepTimeoutMs, 2500), "podcast modal advance");
21642
+ if (!advance.ok) {
21643
+ lastDiag = await clickPodcastModalDomFallback(deps.page);
21644
+ if (!lastDiag.startsWith("{"))
21645
+ throw new Error(advance.error);
21646
+ usedFallback = true;
21647
+ }
21648
+ } catch {
21649
+ lastDiag = await clickPodcastModalDomFallback(deps.page);
21650
+ usedFallback = true;
21651
+ }
21652
+ if (await waitUntil(async () => {
21653
+ if (await closed())
21654
+ return true;
21655
+ if (usedFallback) {
21656
+ lastDiag = await clickPodcastModalDomFallback(deps.page);
21657
+ }
21658
+ return false;
21659
+ }, deps.stepTimeoutMs)) {
21595
21660
  return;
21596
- if (deps.publishStatus === "public") {
21597
- const advance = await clickLightControl(deps.page, PODCAST_MODAL_ADVANCE, deps.stepTimeoutMs, "podcast modal advance");
21598
- if (!advance.ok)
21599
- throw new Error(advance.error);
21600
- if (await waitUntil(closed, deps.stepTimeoutMs))
21601
- return;
21602
21661
  }
21603
- throw new Error(`podcast modal: the \u53D1\u64AD\u5BA2 modal was still open ${deps.stepTimeoutMs}ms after the audio and cover were attached; its post-attach save path has never been measured (task 0236 R3 \u2014 the attended walk is still pending) and the only recorded advance control is '${PODCAST_MODAL_ADVANCE[0]}', which a draft run does not click (it is publish-adjacent)`);
21662
+ const diagSuffix = lastDiag ? ` [modal=${lastDiag}]` : "";
21663
+ throw new Error(`podcast modal: the \u53D1\u64AD\u5BA2 modal stayed open ${deps.stepTimeoutMs}ms after the advance click; the measured 2026-11-06 flow moves to the podcast editor page with the closed-shadow '${DRAFT_LABELS[0]}' bar, so a fast re-measure with a fresh probe is the next step, not a retry${diagSuffix}`);
21604
21664
  }
21605
21665
  async function composePodcast(input, deps) {
21606
21666
  await enterMode(deps, input.operation);
@@ -21609,10 +21669,102 @@ async function composePodcast(input, deps) {
21609
21669
  await findSelector(deps.page, PODCAST_MODAL, "podcast modal", deps.stepTimeoutMs);
21610
21670
  const audioInput = await findAttachedSelector(deps.page, PODCAST_AUDIO_INPUT, "podcast audio input", deps.stepTimeoutMs);
21611
21671
  await uploadFile(asPlaywrightPage(deps.page), audioInput, input.audio, { timeoutMs: deps.stepTimeoutMs });
21612
- const coverInput = await findAttachedSelector(deps.page, PODCAST_COVER_INPUT, "podcast cover input", deps.stepTimeoutMs);
21613
- await uploadFile(asPlaywrightPage(deps.page), coverInput, input.cover, { timeoutMs: deps.stepTimeoutMs });
21672
+ const ack = await waitUntil(async () => {
21673
+ try {
21674
+ return await deps.page.locator(`text=${PODCAST_UPLOAD_ACK}`).count() > 0;
21675
+ } catch {
21676
+ return false;
21677
+ }
21678
+ }, deps.stepTimeoutMs);
21679
+ if (!ack) {
21680
+ throw new Error(`podcast modal: the audio card never showed '${PODCAST_UPLOAD_ACK}' within ${deps.stepTimeoutMs}ms of the file assignment; mimic-file upload of ${input.audio.split("/").pop()} did not register`);
21681
+ }
21682
+ await uploadPodcastCover(deps, input.cover);
21683
+ await podcastAdvanceReady(deps);
21614
21684
  await leavePodcastModal(deps);
21615
21685
  }
21686
+ async function uploadPodcastCover(deps, cover) {
21687
+ const zone = deps.page.locator(PODCAST_COVER_ZONE).first();
21688
+ const box = await zone.boundingBox().catch(() => null);
21689
+ if (!box) {
21690
+ throw new Error(`podcast modal: the cover card ('${PODCAST_COVER_ZONE}') has no bounding box to click`);
21691
+ }
21692
+ if (!deps.page.mouse) {
21693
+ throw new Error("podcast modal: the page seam does not expose a raw mouse for the cover-card coordinate click");
21694
+ }
21695
+ const chooser = deps.page.waitForEvent?.("filechooser", { timeout: 8000 }).catch(() => null);
21696
+ await deps.page.mouse.click(box.x + box.width / 2, box.y + box.height / 2);
21697
+ const pending = Symbol("pending");
21698
+ const quick = await Promise.race([
21699
+ chooser,
21700
+ new Promise((resolve) => setTimeout(() => resolve(pending), 400))
21701
+ ]);
21702
+ let diag = "";
21703
+ if (quick === pending) {
21704
+ const fallback = await deps.page.evaluate(`(() => {
21705
+ const modal = document.querySelector('.d-modal') || document.body;
21706
+ const inputs = Array.from(modal.querySelectorAll('input[type="file"]'));
21707
+ const coverInput = inputs.find((i) => !String(i.getAttribute('accept') || '').includes('.mp3'));
21708
+ const all = Array.from(modal.querySelectorAll('*')).filter((el) => (el.textContent || '').includes('\u8BBE\u7F6E\u5C01\u9762'));
21709
+ const leaf = all[all.length - 1];
21710
+ let card = leaf;
21711
+ while (card && card.parentElement && card.parentElement !== modal) {
21712
+ const pt = card.parentElement.textContent || '';
21713
+ if (pt.includes('\u4E0A\u4F20\u6210\u529F') || pt.includes('\u70B9\u51FB\u4E0A\u4F20') || pt.includes('\u53BB\u53D1\u5E03')) break;
21714
+ card = card.parentElement;
21715
+ }
21716
+ let clickedInput = false;
21717
+ if (coverInput && typeof coverInput.click === 'function') {
21718
+ coverInput.click();
21719
+ clickedInput = true;
21720
+ } else {
21721
+ if (card && typeof card.click === 'function') card.click();
21722
+ if (leaf && typeof leaf.click === 'function') leaf.click();
21723
+ }
21724
+ const rect = card ? card.getBoundingClientRect() : null;
21725
+ return JSON.stringify({
21726
+ clickedInput,
21727
+ x: rect ? Math.round(rect.left + rect.width / 2) : null,
21728
+ y: rect ? Math.round(rect.top + rect.height * 0.35) : null,
21729
+ inputs: inputs.map((i) => ({ accept: i.getAttribute('accept'), cls: i.className })),
21730
+ cardTag: card ? card.tagName + '.' + String(card.className) : null,
21731
+ cardRect: rect ? [Math.round(rect.left), Math.round(rect.top), Math.round(rect.width), Math.round(rect.height)] : null,
21732
+ });
21733
+ })()`).catch(() => "");
21734
+ if (typeof fallback === "string" && fallback.startsWith("{")) {
21735
+ diag = ` [modal=${fallback}]`;
21736
+ try {
21737
+ const parsed = JSON.parse(fallback);
21738
+ if (!parsed.clickedInput && typeof parsed.x === "number" && typeof parsed.y === "number") {
21739
+ await deps.page.mouse.click(parsed.x, parsed.y);
21740
+ }
21741
+ } catch {}
21742
+ }
21743
+ }
21744
+ const opened = quick === pending ? await chooser : quick;
21745
+ if (!opened) {
21746
+ throw new Error(`podcast modal: clicking the cover card did not open a native file chooser within 8000ms${diag}`);
21747
+ }
21748
+ await opened.setFiles(cover);
21749
+ }
21750
+ async function podcastAdvanceReady(deps) {
21751
+ if (deps.publishStatus !== "draft") {
21752
+ return;
21753
+ }
21754
+ const enabled = await waitUntil(async () => {
21755
+ try {
21756
+ if (await deps.page.locator('button:has-text("\u53BB\u53D1\u5E03")').count() === 0)
21757
+ return false;
21758
+ const disabled = await deps.page.locator('button:has-text("\u53BB\u53D1\u5E03")[disabled], button:has-text("\u53BB\u53D1\u5E03")[aria-disabled="true"]').count();
21759
+ return disabled === 0;
21760
+ } catch {
21761
+ return false;
21762
+ }
21763
+ }, deps.stepTimeoutMs);
21764
+ if (!enabled) {
21765
+ throw new Error(`podcast modal: '${PODCAST_MODAL_ADVANCE[0]}' stayed disabled \u2014 the cards were not both accepted; check the audio ack '${PODCAST_UPLOAD_ACK}' and retry with the cover chooser path`);
21766
+ }
21767
+ }
21616
21768
  async function compose(input, deps) {
21617
21769
  switch (input.operation) {
21618
21770
  case "\u4E0A\u4F20\u56FE\u6587":
@@ -20996,6 +20996,7 @@ function wrapPlaywrightPage(raw, shadow) {
20996
20996
  isVisible: (options) => loc.isVisible(options),
20997
20997
  fill: (text, options) => loc.fill(text, options),
20998
20998
  click: (options) => loc.click(options),
20999
+ boundingBox: async () => (await onScreen(selector)).at(-1)?.box ?? await loc.boundingBox?.() ?? null,
20999
21000
  type: (text, options) => loc.pressSequentially(text, options),
21000
21001
  textContent: () => loc.textContent()
21001
21002
  };
@@ -21014,7 +21015,16 @@ function wrapPlaywrightPage(raw, shadow) {
21014
21015
  return true;
21015
21016
  },
21016
21017
  findShadowControl: async (labels) => shadow ? shadow.findControl(labels) : null,
21017
- clickAt: (x, y) => pw.mouse.click(x, y)
21018
+ clickAt: (x, y) => pw.mouse.click(x, y),
21019
+ waitForEvent: (event, options) => {
21020
+ if (typeof pw.waitForEvent !== "function") {
21021
+ return Promise.reject(new Error(`the page seam does not expose a '${event}' event source`));
21022
+ }
21023
+ return pw.waitForEvent(event, options);
21024
+ },
21025
+ mouse: {
21026
+ click: (x, y) => pw.mouse.click(x, y)
21027
+ }
21018
21028
  };
21019
21029
  }
21020
21030