@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
@@ -14671,7 +14671,7 @@ var init_zod = __esm(() => {
14671
14671
  init_external();
14672
14672
  });
14673
14673
 
14674
- // ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.11/node_modules/@gobing-ai/ts-utils/dist/errors.js
14674
+ // ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.18/node_modules/@gobing-ai/ts-utils/dist/errors.js
14675
14675
  var ErrorCode;
14676
14676
  var init_errors3 = __esm(() => {
14677
14677
  ErrorCode = {
@@ -14682,7 +14682,7 @@ var init_errors3 = __esm(() => {
14682
14682
  };
14683
14683
  });
14684
14684
 
14685
- // ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.11/node_modules/@gobing-ai/ts-utils/dist/api-response.js
14685
+ // ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.18/node_modules/@gobing-ai/ts-utils/dist/api-response.js
14686
14686
  var API_ERROR_CODES, ERROR_CODE_TO_HTTP, CLIENT_SAFE_CODES;
14687
14687
  var init_api_response = __esm(() => {
14688
14688
  init_errors3();
@@ -14705,10 +14705,10 @@ var init_api_response = __esm(() => {
14705
14705
  CLIENT_SAFE_CODES = new Set([ErrorCode.NotFound, ErrorCode.Validation, ErrorCode.Conflict]);
14706
14706
  });
14707
14707
 
14708
- // ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.11/node_modules/@gobing-ai/ts-utils/dist/cursor.js
14708
+ // ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.18/node_modules/@gobing-ai/ts-utils/dist/cursor.js
14709
14709
  var init_cursor = () => {};
14710
14710
 
14711
- // ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.11/node_modules/@gobing-ai/ts-utils/dist/env.js
14711
+ // ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.18/node_modules/@gobing-ai/ts-utils/dist/env.js
14712
14712
  function getEnvVar(name, fallback) {
14713
14713
  const raw = process.env[name];
14714
14714
  return raw === undefined ? fallback : raw;
@@ -14717,10 +14717,10 @@ function getEnvVars() {
14717
14717
  return process.env;
14718
14718
  }
14719
14719
 
14720
- // ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.11/node_modules/@gobing-ai/ts-utils/dist/event-severity.js
14720
+ // ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.18/node_modules/@gobing-ai/ts-utils/dist/event-severity.js
14721
14721
  var init_event_severity = () => {};
14722
14722
 
14723
- // ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.11/node_modules/@gobing-ai/ts-utils/dist/object.js
14723
+ // ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.18/node_modules/@gobing-ai/ts-utils/dist/object.js
14724
14724
  function isPlainObject2(value) {
14725
14725
  return typeof value === "object" && value !== null && !Array.isArray(value);
14726
14726
  }
@@ -14738,7 +14738,7 @@ function deepMerge(target, source) {
14738
14738
  return result;
14739
14739
  }
14740
14740
 
14741
- // ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.11/node_modules/@gobing-ai/ts-utils/dist/output.js
14741
+ // ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.18/node_modules/@gobing-ai/ts-utils/dist/output.js
14742
14742
  function processStream(name) {
14743
14743
  const proc = globalThis.process;
14744
14744
  const stream = proc?.[name];
@@ -14756,7 +14756,7 @@ function echoError(message, target = defaultStderrTarget ?? processStream("stder
14756
14756
  }
14757
14757
  var defaultStderrTarget;
14758
14758
 
14759
- // ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.11/node_modules/@gobing-ai/ts-utils/dist/index.js
14759
+ // ../../node_modules/.bun/@gobing-ai+ts-utils@0.5.18/node_modules/@gobing-ai/ts-utils/dist/index.js
14760
14760
  var init_dist = __esm(() => {
14761
14761
  init_api_response();
14762
14762
  init_cursor();
@@ -21754,7 +21754,7 @@ var init_dist2 = __esm(() => {
21754
21754
  $visitAsync = visit.visitAsync;
21755
21755
  });
21756
21756
 
21757
- // ../../node_modules/.bun/@gobing-ai+ts-runtime@0.5.11/node_modules/@gobing-ai/ts-runtime/dist/config.js
21757
+ // ../../node_modules/.bun/@gobing-ai+ts-runtime@0.5.18+ea28d47622d670ff/node_modules/@gobing-ai/ts-runtime/dist/config.js
21758
21758
  function getProcessEnv() {
21759
21759
  return getEnvVars();
21760
21760
  }
@@ -21832,7 +21832,7 @@ var init_config = __esm(() => {
21832
21832
  };
21833
21833
  });
21834
21834
 
21835
- // ../../node_modules/.bun/@gobing-ai+ts-runtime@0.5.11/node_modules/@gobing-ai/ts-runtime/dist/file-system-node.js
21835
+ // ../../node_modules/.bun/@gobing-ai+ts-runtime@0.5.18+ea28d47622d670ff/node_modules/@gobing-ai/ts-runtime/dist/file-system-node.js
21836
21836
  import { appendFileSync, cpSync, createReadStream, createWriteStream, existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from "fs";
21837
21837
  import { dirname, resolve as resolvePath } from "path";
21838
21838
  function createNodeFileSystem(root, paths) {
@@ -21919,7 +21919,7 @@ function findProjectRoot(startDir) {
21919
21919
  }
21920
21920
  var init_file_system_node = () => {};
21921
21921
 
21922
- // ../../node_modules/.bun/@gobing-ai+ts-runtime@0.5.11/node_modules/@gobing-ai/ts-runtime/dist/db-errors.js
21922
+ // ../../node_modules/.bun/@gobing-ai+ts-runtime@0.5.18+ea28d47622d670ff/node_modules/@gobing-ai/ts-runtime/dist/db-errors.js
21923
21923
  var DbModuleNotInstalledError;
21924
21924
  var init_db_errors = __esm(() => {
21925
21925
  DbModuleNotInstalledError = class DbModuleNotInstalledError extends Error {
@@ -29130,7 +29130,7 @@ var init_execa = __esm(() => {
29130
29130
  } = getIpcExport());
29131
29131
  });
29132
29132
 
29133
- // ../../node_modules/.bun/@gobing-ai+ts-runtime@0.5.11/node_modules/@gobing-ai/ts-runtime/dist/process-group.js
29133
+ // ../../node_modules/.bun/@gobing-ai+ts-runtime@0.5.18+ea28d47622d670ff/node_modules/@gobing-ai/ts-runtime/dist/process-group.js
29134
29134
  function resolveDeadline(optionTimeout, defaultTimeout) {
29135
29135
  if (optionTimeout === null)
29136
29136
  return null;
@@ -29222,7 +29222,7 @@ function ownProcessGroupLifecycle(ownership) {
29222
29222
  }
29223
29223
  var MAX_TIMEOUT_MS = 2147483647, DEFAULT_KILL_GRACE_MS = 5000, GROUP_POLL_INTERVAL_MS = 25, GROUP_KILL_PATIENCE_MS = 2000;
29224
29224
 
29225
- // ../../node_modules/.bun/@gobing-ai+ts-runtime@0.5.11/node_modules/@gobing-ai/ts-runtime/dist/process-executor.js
29225
+ // ../../node_modules/.bun/@gobing-ai+ts-runtime@0.5.18+ea28d47622d670ff/node_modules/@gobing-ai/ts-runtime/dist/process-executor.js
29226
29226
  import { isatty } from "tty";
29227
29227
  function processEventSeverity(reason, exitCode) {
29228
29228
  if (reason === "error")
@@ -29594,7 +29594,7 @@ var init_process_executor = __esm(() => {
29594
29594
  init_config();
29595
29595
  });
29596
29596
 
29597
- // ../../node_modules/.bun/@gobing-ai+ts-runtime@0.5.11/node_modules/@gobing-ai/ts-runtime/dist/runtime-node-bun.js
29597
+ // ../../node_modules/.bun/@gobing-ai+ts-runtime@0.5.18+ea28d47622d670ff/node_modules/@gobing-ai/ts-runtime/dist/runtime-node-bun.js
29598
29598
  function getNodeFileSystem() {
29599
29599
  if (!_nodeFileSystem)
29600
29600
  _nodeFileSystem = createNodeFileSystem();
@@ -29655,7 +29655,7 @@ var init_runtime_node_bun = __esm(() => {
29655
29655
  });
29656
29656
 
29657
29657
  // ../../plugins/publishings/xhs-pub/src/index.ts
29658
- import { dirname as dirname2, isAbsolute } from "path";
29658
+ import { dirname as dirname2 } from "path";
29659
29659
  import { parseArgs } from "util";
29660
29660
 
29661
29661
  // ../../packages/kk-core/src/kinds.ts
@@ -29741,15 +29741,16 @@ var PluginManifestSchema = exports_external.looseObject({
29741
29741
  name: exports_external.string().min(1),
29742
29742
  kind: PluginKindSchema,
29743
29743
  entry: exports_external.string().min(1),
29744
+ check: exports_external.string().min(1).optional(),
29744
29745
  version: exports_external.string().optional(),
29745
29746
  description: exports_external.string().optional(),
29746
29747
  operations: exports_external.record(exports_external.string(), PluginOperationSchema).optional()
29747
29748
  });
29748
- // ../../node_modules/.bun/@gobing-ai+ts-runtime@0.5.11/node_modules/@gobing-ai/ts-runtime/dist/index.js
29749
+ // ../../node_modules/.bun/@gobing-ai+ts-runtime@0.5.18+ea28d47622d670ff/node_modules/@gobing-ai/ts-runtime/dist/index.js
29749
29750
  init_file_system_node();
29750
29751
  init_config();
29751
29752
 
29752
- // ../../node_modules/.bun/@gobing-ai+ts-runtime@0.5.11/node_modules/@gobing-ai/ts-runtime/dist/path.js
29753
+ // ../../node_modules/.bun/@gobing-ai+ts-runtime@0.5.18+ea28d47622d670ff/node_modules/@gobing-ai/ts-runtime/dist/path.js
29753
29754
  function normalizeSeparators(path6) {
29754
29755
  return path6.replaceAll("\\", "/");
29755
29756
  }
@@ -29807,7 +29808,7 @@ function getProcessCwd() {
29807
29808
  return globalThis.process?.cwd?.() ?? "/";
29808
29809
  }
29809
29810
 
29810
- // ../../node_modules/.bun/@gobing-ai+ts-runtime@0.5.11/node_modules/@gobing-ai/ts-runtime/dist/index.js
29811
+ // ../../node_modules/.bun/@gobing-ai+ts-runtime@0.5.18+ea28d47622d670ff/node_modules/@gobing-ai/ts-runtime/dist/index.js
29811
29812
  init_runtime_node_bun();
29812
29813
 
29813
29814
  // ../../packages/kk-core/src/render-md.ts
@@ -29919,7 +29920,11 @@ async function trySelectors(page, selectors, options = {}) {
29919
29920
  }
29920
29921
  return { found: false, selector: null };
29921
29922
  }
29922
- // ../../node_modules/.bun/@gobing-ai+ts-browser-automation@0.5.11+e28deaf1f2c5a2b1/node_modules/@gobing-ai/ts-browser-automation/dist/errors.js
29923
+
29924
+ // ../../packages/publish-harness/src/browser.ts
29925
+ import { resolve as resolve2 } from "path";
29926
+
29927
+ // ../../node_modules/.bun/@gobing-ai+ts-browser-automation@0.5.18+1f5782d4d327a230/node_modules/@gobing-ai/ts-browser-automation/dist/errors.js
29923
29928
  class BrowserAutomationError extends Error {
29924
29929
  constructor(message, options) {
29925
29930
  super(message, options);
@@ -29968,11 +29973,10 @@ class OperationAbortedError extends BrowserAutomationError {
29968
29973
  this.name = "OperationAbortedError";
29969
29974
  }
29970
29975
  }
29971
-
29972
- // ../../node_modules/.bun/@gobing-ai+ts-browser-automation@0.5.11+e28deaf1f2c5a2b1/node_modules/@gobing-ai/ts-browser-automation/dist/launcher.js
29976
+ // ../../node_modules/.bun/@gobing-ai+ts-browser-automation@0.5.18+1f5782d4d327a230/node_modules/@gobing-ai/ts-browser-automation/dist/launcher.js
29973
29977
  import { mkdir } from "fs/promises";
29974
29978
 
29975
- // ../../node_modules/.bun/@gobing-ai+ts-browser-automation@0.5.11+e28deaf1f2c5a2b1/node_modules/@gobing-ai/ts-browser-automation/dist/profile-dir.js
29979
+ // ../../node_modules/.bun/@gobing-ai+ts-browser-automation@0.5.18+1f5782d4d327a230/node_modules/@gobing-ai/ts-browser-automation/dist/profile-dir.js
29976
29980
  function resolveProfileDir(profileDir) {
29977
29981
  if (profileDir.trim() === "") {
29978
29982
  throw new InvalidProfileDirError;
@@ -29984,7 +29988,7 @@ function resolveProfileDir(profileDir) {
29984
29988
  return resolved;
29985
29989
  }
29986
29990
 
29987
- // ../../node_modules/.bun/@gobing-ai+ts-browser-automation@0.5.11+e28deaf1f2c5a2b1/node_modules/@gobing-ai/ts-browser-automation/dist/launcher.js
29991
+ // ../../node_modules/.bun/@gobing-ai+ts-browser-automation@0.5.18+1f5782d4d327a230/node_modules/@gobing-ai/ts-browser-automation/dist/launcher.js
29988
29992
  var PROFILE_DIR_MODE = 448;
29989
29993
  var BUSY_PROFILE_PATTERN = /singleton|already in use|in use by another/i;
29990
29994
  var CHROMIUM_MISSING_PATTERN = /playwright install|executable doesn't exist/i;
@@ -30035,7 +30039,7 @@ function createPersistentLauncher(options = {}) {
30035
30039
  };
30036
30040
  }
30037
30041
 
30038
- // ../../node_modules/.bun/@gobing-ai+ts-browser-automation@0.5.11+e28deaf1f2c5a2b1/node_modules/@gobing-ai/ts-browser-automation/dist/operations.js
30042
+ // ../../node_modules/.bun/@gobing-ai+ts-browser-automation@0.5.18+1f5782d4d327a230/node_modules/@gobing-ai/ts-browser-automation/dist/operations.js
30039
30043
  var DEFAULT_TIMEOUT_MS = 30000;
30040
30044
  var POLL_INTERVAL_MS = 250;
30041
30045
  var defaultLauncher;
@@ -30044,12 +30048,6 @@ function getDefaultLauncher() {
30044
30048
  defaultLauncher = createPersistentLauncher();
30045
30049
  return defaultLauncher;
30046
30050
  }
30047
- async function openPersistentBrowser(options) {
30048
- return (options.launcher ?? getDefaultLauncher()).launch({
30049
- profileDir: resolveProfileDir(options.profileDir),
30050
- headless: options.headless
30051
- });
30052
- }
30053
30051
  async function withProfilePage(options, callback) {
30054
30052
  assertNotAborted(options.signal);
30055
30053
  if (options.isAuthenticated !== undefined)
@@ -30120,19 +30118,270 @@ async function pollPredicate(page, predicate, timeoutMs, signal) {
30120
30118
  function sleep(ms) {
30121
30119
  return new Promise((resolve2) => setTimeout(resolve2, ms));
30122
30120
  }
30121
+ // ../../packages/publish-harness/src/profile-dir.ts
30122
+ import { mkdir as mkdir2, stat } from "fs/promises";
30123
+ async function ensureProfileDir2(profileDir, headless) {
30124
+ const existing = await stat(profileDir).catch(() => null);
30125
+ if (existing) {
30126
+ if (!existing.isDirectory())
30127
+ throw new InvalidProfileDirError(`profileDir is not a directory: "${profileDir}".`);
30128
+ return;
30129
+ }
30130
+ if (headless)
30131
+ throw new BrowserProfileMissingError(profileDir);
30132
+ await mkdir2(profileDir, { recursive: true, mode: 448 });
30133
+ }
30134
+
30135
+ // ../../packages/publish-harness/src/browser.ts
30136
+ var DEFAULT_CHECK_TIMEOUT_MS = 30000;
30137
+ var DEFAULT_STEP_TIMEOUT_MS = 30000;
30138
+ var DEFAULT_POLL_MS = 500;
30139
+ var ATTACH_POLL_MS = 100;
30140
+ function createKkBrowserLauncher(loadPlaywright = () => import("playwright")) {
30141
+ return {
30142
+ async launch(request) {
30143
+ if (request.profileDir.trim() === "")
30144
+ throw new InvalidProfileDirError;
30145
+ const profileDir = resolve2(request.profileDir);
30146
+ await ensureProfileDir2(profileDir, request.headless);
30147
+ let playwright;
30148
+ try {
30149
+ playwright = await loadPlaywright();
30150
+ } catch (error51) {
30151
+ throw new PlaywrightUnavailableError('The "playwright" dependency is not installed. Run `bunx playwright install chromium`.', { cause: error51 });
30152
+ }
30153
+ const channel = process.env.KK_BROWSER_CHANNEL?.trim() || "chromium";
30154
+ let context2;
30155
+ try {
30156
+ context2 = await playwright.chromium.launchPersistentContext(profileDir, {
30157
+ headless: request.headless,
30158
+ channel,
30159
+ args: ["--disable-blink-features=AutomationControlled"],
30160
+ ignoreDefaultArgs: ["--enable-automation"]
30161
+ });
30162
+ } catch (error51) {
30163
+ const message = errorMessage2(error51);
30164
+ if (/singleton|already in use|in use by another/i.test(message)) {
30165
+ throw new BrowserProfileBusyError(profileDir, error51);
30166
+ }
30167
+ if (/playwright install|executable doesn't exist/i.test(message)) {
30168
+ throw new PlaywrightUnavailableError(`Browser channel '${channel}' is not installed. Run \`bunx playwright install chromium\` (or install Google Chrome for KK_BROWSER_CHANNEL=chrome).`, { cause: error51 });
30169
+ }
30170
+ throw error51;
30171
+ }
30172
+ await context2.addInitScript(() => {
30173
+ Object.defineProperty(navigator, "webdriver", { get: () => {
30174
+ return;
30175
+ } });
30176
+ });
30177
+ return context2;
30178
+ }
30179
+ };
30180
+ }
30181
+ var kkBrowserLauncher = createKkBrowserLauncher();
30182
+ function resolveChannelProfileDir(channel, override) {
30183
+ const tiers = [override, process.env[`${channel.toUpperCase()}_PROFILE_DIR`], process.env.KK_BROWSER_PROFILE_DIR];
30184
+ const chosen = tiers.find((tier) => tier !== undefined && tier.trim().length > 0);
30185
+ return resolveBrowserProfileDir(channel, chosen);
30186
+ }
30187
+ async function checkChannelSession(config3, opts) {
30188
+ const timeoutMs = opts.timeoutMs ?? DEFAULT_CHECK_TIMEOUT_MS;
30189
+ const passes = opts.headedOnly ? [false] : opts.headedFallback === false ? [true] : [true, false];
30190
+ let lastDetail = `${config3.channel} readiness was never probed`;
30191
+ for (const headless of passes) {
30192
+ try {
30193
+ await withProfilePage({
30194
+ profileDir: opts.profileDir,
30195
+ headless,
30196
+ url: config3.homeUrl,
30197
+ isAuthenticated: config3.isAuthenticated,
30198
+ timeoutMs,
30199
+ launcher: opts.launcher ?? kkBrowserLauncher
30200
+ }, async () => true);
30201
+ return {
30202
+ status: "verified",
30203
+ profileDir: opts.profileDir,
30204
+ detail: `authenticated ${config3.channel} session observed ${headless ? "headlessly" : "via a headed pass"} via profile ${opts.profileDir} (read-only)`
30205
+ };
30206
+ } catch (error51) {
30207
+ if (error51 instanceof BrowserProfileMissingError) {
30208
+ return {
30209
+ status: "refused",
30210
+ profileDir: opts.profileDir,
30211
+ detail: `no ${config3.channel} browser profile at ${opts.profileDir} \u2014 run the login command first`
30212
+ };
30213
+ }
30214
+ lastDetail = `${headless ? "headless" : "headed"} ${config3.channel} readiness refused: ${errorMessage2(error51)}`;
30215
+ }
30216
+ }
30217
+ return { status: "refused", profileDir: opts.profileDir, detail: lastDetail };
30218
+ }
30219
+ async function waitUntil(predicate, timeoutMs, pollMs = DEFAULT_POLL_MS) {
30220
+ const deadline = Date.now() + timeoutMs;
30221
+ while (Date.now() < deadline) {
30222
+ if (await predicate()) {
30223
+ return true;
30224
+ }
30225
+ await new Promise((resolve3) => setTimeout(resolve3, pollMs));
30226
+ }
30227
+ return predicate();
30228
+ }
30229
+ async function pasteHtml(page, selector, html, opts = {}) {
30230
+ const timeoutMs = opts.timeoutMs ?? DEFAULT_STEP_TIMEOUT_MS;
30231
+ await requireAttached(page, selector, "pasteHtml", timeoutMs);
30232
+ await page.evaluate(pasteScript(selector, html));
30233
+ const filled = await waitUntil(async () => String(await page.evaluate(editorTextScript(selector))).length > 0, timeoutMs, ATTACH_POLL_MS);
30234
+ if (!filled) {
30235
+ throw new Error(`pasteHtml: ${selector}: editor text is empty ${timeoutMs}ms after the paste`);
30236
+ }
30237
+ }
30238
+ async function uploadFile(page, selector, filePath, opts = {}) {
30239
+ const timeoutMs = opts.timeoutMs ?? DEFAULT_STEP_TIMEOUT_MS;
30240
+ await requireAttached(page, selector, "uploadFile", timeoutMs);
30241
+ await page.setInputFiles(selector, filePath);
30242
+ }
30243
+ async function expectOutcome(probe, opts) {
30244
+ let evidence = "";
30245
+ let lastProbeError = null;
30246
+ const observed = await waitUntil(async () => {
30247
+ let result;
30248
+ try {
30249
+ result = await probe();
30250
+ } catch (error51) {
30251
+ lastProbeError = error51 instanceof Error ? error51.message : String(error51);
30252
+ return false;
30253
+ }
30254
+ if (result === null || result.length === 0) {
30255
+ return false;
30256
+ }
30257
+ evidence = result;
30258
+ return true;
30259
+ }, opts.timeoutMs, opts.pollMs ?? DEFAULT_POLL_MS);
30260
+ if (observed && evidence.length > 0) {
30261
+ return { ok: true, evidence };
30262
+ }
30263
+ const detail = lastProbeError === null ? "" : ` (last probe error: ${lastProbeError})`;
30264
+ return { ok: false, error: `${opts.label}: no outcome observed within ${opts.timeoutMs}ms${detail}` };
30265
+ }
30266
+ var DEFAULT_SIGNAL_CONTAINERS = [
30267
+ '[class*="toast" i]',
30268
+ '[class*="notification" i]',
30269
+ '[role="alert"]',
30270
+ '[class*="message" i]'
30271
+ ];
30272
+ async function readScopedSignal(page, texts, containers = DEFAULT_SIGNAL_CONTAINERS) {
30273
+ for (const text of texts) {
30274
+ for (const container of containers) {
30275
+ const locator = page.locator(`${container} :text("${text}")`).first();
30276
+ if (await locator.count().catch(() => 0) === 0)
30277
+ continue;
30278
+ if (!await locator.isVisible().catch(() => false))
30279
+ continue;
30280
+ const observed = (await locator.textContent().catch(() => null))?.trim();
30281
+ return observed !== undefined && observed.length > 0 ? observed : text;
30282
+ }
30283
+ }
30284
+ return null;
30285
+ }
30286
+ var cdpNodeText = (node) => node.nodeName === "#text" ? node.nodeValue ?? "" : (node.children ?? []).map(cdpNodeText).join("");
30287
+ async function findShadowControl(session, labels) {
30288
+ const document = await session.send("DOM.getDocument", { depth: -1, pierce: true });
30289
+ if (!document.root)
30290
+ return null;
30291
+ const wanted = new Set(labels);
30292
+ const findNode = (node, insideShadow) => {
30293
+ if (insideShadow && node.nodeName === "BUTTON" && typeof node.backendNodeId === "number") {
30294
+ const label = cdpNodeText(node).replace(/\s+/g, "");
30295
+ if (wanted.has(label))
30296
+ return { label, backendNodeId: node.backendNodeId };
30297
+ }
30298
+ for (const shadowRoot of node.shadowRoots ?? []) {
30299
+ const hit2 = findNode(shadowRoot, true);
30300
+ if (hit2)
30301
+ return hit2;
30302
+ }
30303
+ for (const child of node.children ?? []) {
30304
+ const hit2 = findNode(child, insideShadow);
30305
+ if (hit2)
30306
+ return hit2;
30307
+ }
30308
+ return null;
30309
+ };
30310
+ const hit = findNode(document.root, false);
30311
+ if (hit === null)
30312
+ return null;
30313
+ const box = await session.send("DOM.getBoxModel", { backendNodeId: hit.backendNodeId });
30314
+ const quad = box.model?.content;
30315
+ if (quad === undefined || quad.length < 8)
30316
+ return null;
30317
+ const [x1 = 0, y1 = 0, x3 = 0, , , y4 = 0] = quad;
30318
+ return { label: hit.label, x: Math.round((x1 + x3) / 2), y: Math.round((y1 + y4) / 2) };
30319
+ }
30320
+ async function createShadowFinder(context2, page) {
30321
+ if (typeof context2.newCDPSession !== "function")
30322
+ return;
30323
+ try {
30324
+ const session = await context2.newCDPSession(page);
30325
+ await session.send("DOM.enable");
30326
+ return (labels) => findShadowControl(session, labels);
30327
+ } catch {
30328
+ return;
30329
+ }
30330
+ }
30331
+ var markdownToHtml = (markdown) => Bun.markdown.html(markdown);
30332
+ async function requireAttached(page, selector, action, timeoutMs) {
30333
+ await resolveSelector(page, [selector], { timeoutMs, label: action });
30334
+ }
30335
+ async function resolveSelector(page, selectors, opts = {}) {
30336
+ const timeoutMs = opts.timeoutMs ?? DEFAULT_STEP_TIMEOUT_MS;
30337
+ const found = { selector: null };
30338
+ const attached = await waitUntil(async () => {
30339
+ const result = await trySelectors(page, selectors, { visible: false });
30340
+ found.selector = result.selector;
30341
+ return result.found && result.selector !== null;
30342
+ }, timeoutMs, ATTACH_POLL_MS);
30343
+ const selector = found.selector;
30344
+ if (!attached || selector === null) {
30345
+ throw new Error(`${opts.label ?? "selector"}: ${selectors.join(" | ")} not attached within ${timeoutMs}ms`);
30346
+ }
30347
+ return selector;
30348
+ }
30349
+ function errorMessage2(error51) {
30350
+ return error51 instanceof Error ? error51.message : String(error51);
30351
+ }
30352
+ function pasteScript(selector, html) {
30353
+ const text = html.replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim();
30354
+ return `(() => {
30355
+ const el = document.querySelector(${JSON.stringify(selector)});
30356
+ if (!el) return false;
30357
+ const data = new DataTransfer();
30358
+ data.setData('text/html', ${JSON.stringify(html)});
30359
+ data.setData('text/plain', ${JSON.stringify(text)});
30360
+ el.focus();
30361
+ el.dispatchEvent(new ClipboardEvent('paste', { clipboardData: data, bubbles: true, cancelable: true }));
30362
+ return true;
30363
+ })()`;
30364
+ }
30365
+ function editorTextScript(selector) {
30366
+ return `(() => {
30367
+ const el = document.querySelector(${JSON.stringify(selector)});
30368
+ return el ? (el.textContent ?? '').trim() : '';
30369
+ })()`;
30370
+ }
30371
+
30123
30372
  // ../../plugins/publishings/xhs-pub/src/index.ts
30124
30373
  init_dist();
30125
30374
 
30126
30375
  // ../../plugins/publishings/xhs-pub/src/automation-guard.ts
30127
30376
  import { homedir as homedir2 } from "os";
30128
- import { join as join2, resolve as resolve2 } from "path";
30377
+ import { join as join2, resolve as resolve3 } from "path";
30129
30378
  var fs = createNodeFileSystem();
30130
30379
  var CAP_WINDOW_MS = 2 * 60 * 60 * 1000;
30131
30380
  var AUTOMATION_STATE_ENV = "KK_XHS_AUTOMATION_STATE";
30132
30381
  function resolveAutomationStatePath(override) {
30133
30382
  const explicit = override ?? process.env[AUTOMATION_STATE_ENV];
30134
30383
  if (explicit !== undefined && explicit !== "")
30135
- return resolve2(explicit);
30384
+ return resolve3(explicit);
30136
30385
  return join2(process.env.HOME ?? homedir2(), ".config", "kk", "xhs-automation.json");
30137
30386
  }
30138
30387
  function resetHint(path7) {
@@ -30195,48 +30444,86 @@ function createAutomationGuard(options) {
30195
30444
  };
30196
30445
  }
30197
30446
 
30198
- // ../../plugins/publishings/xhs-pub/src/index.ts
30199
- var CHANNEL = "xhs";
30447
+ // ../../plugins/publishings/xhs-pub/src/modes.ts
30448
+ import { isAbsolute } from "path";
30200
30449
  var TARGET = "xhs";
30201
30450
  var PUBLISH_URL = "https://creator.xiaohongshu.com/publish/publish";
30202
30451
  var PUBLISHED_MARKER = "published=true";
30203
- var LOGIN_TIMEOUT_MS = 120000;
30204
- var STEP_TIMEOUT_MS = 30000;
30205
- var PREFLIGHT_TIMEOUT_MS = 30000;
30206
- var POLL_MS = 500;
30207
- var SUBMIT_SETTLE_MS = 2500;
30208
- var SUBMIT_ATTEMPTS = 3;
30209
- var SELECTORS = {
30210
- imageTab: ['div.creator-tab:has-text("\u56FE\u6587")', "text=\u4E0A\u4F20\u56FE\u6587"],
30211
- fileInput: ['input[type="file"][accept*=".jpg"]', 'input[type="file"]'],
30212
- thumbnail: ["img.preivew-image", ".img-preview img"],
30213
- title: ['input[placeholder*="\u6807\u9898"]', "#title-textarea input"],
30214
- body: [
30215
- 'div.tiptap[contenteditable="true"]',
30216
- 'div[contenteditable="true"].ql-editor',
30217
- "#post-textarea",
30218
- ".ql-editor"
30219
- ],
30220
- draftBox: ["text=/\u8349\u7A3F\u7BB1\\(\\d+\\)/", "text=\u8349\u7A3F\u7BB1"],
30221
- publishConfirm: ["text=\u53D1\u5E03\u6210\u529F", "text=\u5DF2\u53D1\u5E03"]
30222
- };
30223
- var SHADOW_CONTROLS = {
30224
- draft: ["\u6682\u5B58\u79BB\u5F00", "\u5B58\u8349\u7A3F", "\u4FDD\u5B58\u8349\u7A3F"],
30225
- publish: ["\u53D1\u5E03"]
30452
+ var XHS_OPERATIONS = ["\u4E0A\u4F20\u56FE\u6587", "\u4E0A\u4F20\u89C6\u9891", "\u5199\u957F\u6587", "\u53D1\u64AD\u5BA2"];
30453
+ var DEFAULT_XHS_OPERATION = "\u4E0A\u4F20\u56FE\u6587";
30454
+ function resolveXhsOperation(options, content) {
30455
+ const raw = options?.operation ?? content?.metadata?.operation;
30456
+ if (raw === undefined)
30457
+ return { operation: DEFAULT_XHS_OPERATION };
30458
+ if (typeof raw !== "string" || !XHS_OPERATIONS.includes(raw)) {
30459
+ return {
30460
+ error: `unsupported operation '${String(raw)}' for ${TARGET}: expected one of ${XHS_OPERATIONS.join(", ")}`
30461
+ };
30462
+ }
30463
+ return { operation: raw };
30464
+ }
30465
+ var IMAGE_EXTENSIONS = /\.(png|jpe?g|webp)$/i;
30466
+ var IMAGE_LIST = "png|jpg|jpeg|webp";
30467
+ var VIDEO_EXTENSIONS = /\.(mp4|mov|flv|f4v|mkv|rm|rmvb|m4v|mpg|mpeg|ts)$/i;
30468
+ var VIDEO_LIST = "mp4|mov|flv|f4v|mkv|rm|rmvb|m4v|mpg|mpeg|ts";
30469
+ var AUDIO_EXTENSIONS = /\.(mp3|wav|aac|flac|m4a)$/i;
30470
+ var AUDIO_LIST = "mp3|wav|aac|flac|m4a";
30471
+ var XHS_IMAGE_MAX = 18;
30472
+ var XHS_NOTE_TITLE_MAX = 20;
30473
+ var XHS_NOTE_BODY_MAX = 1000;
30474
+ var MODE_LIMITS = {
30475
+ \u{4e0a}\u{4f20}\u{56fe}\u{6587}: { imageMax: XHS_IMAGE_MAX, titleMax: XHS_NOTE_TITLE_MAX, bodyMax: XHS_NOTE_BODY_MAX },
30476
+ \u{4e0a}\u{4f20}\u{89c6}\u{9891}: { titleMax: XHS_NOTE_TITLE_MAX },
30477
+ \u{5199}\u{957f}\u{6587}: {},
30478
+ \u{53d1}\u{64ad}\u{5ba2}: {}
30226
30479
  };
30227
- function fail(error51) {
30228
- return ResultSchema.parse({ ok: false, target: TARGET, error: error51 });
30480
+ function artifactPaths(input) {
30481
+ switch (input.operation) {
30482
+ case "\u4E0A\u4F20\u56FE\u6587":
30483
+ return input.images;
30484
+ case "\u4E0A\u4F20\u89C6\u9891":
30485
+ return [input.video];
30486
+ case "\u5199\u957F\u6587":
30487
+ return [];
30488
+ case "\u53D1\u64AD\u5BA2":
30489
+ return [input.audio, input.cover];
30490
+ }
30229
30491
  }
30230
- async function validatePublishInput(content) {
30231
- const status = content.metadata?.publishStatus;
30232
- if (status !== undefined && status !== "draft" && status !== "public") {
30233
- return { error: `images validation: unknown publishStatus '${String(status)}' (expected 'draft' or 'public')` };
30492
+ async function resolveMediaPath(content, spec) {
30493
+ const raw = content.metadata?.[spec.key];
30494
+ const path7 = typeof raw === "string" ? raw : typeof raw?.path === "string" ? raw.path : undefined;
30495
+ if (path7 === undefined || path7.length === 0) {
30496
+ return {
30497
+ error: `${spec.label}: Content.metadata.${spec.key} is required (absolute path to an existing ${spec.list} file)`
30498
+ };
30234
30499
  }
30500
+ if (!isAbsolute(path7)) {
30501
+ return { error: `${spec.label}: Content.metadata.${spec.key} must be absolute, got '${path7}'` };
30502
+ }
30503
+ if (!spec.extensions.test(path7)) {
30504
+ return {
30505
+ error: `${spec.label}: Content.metadata.${spec.key} has unsupported extension (expected ${spec.list}): '${path7}'`
30506
+ };
30507
+ }
30508
+ if (!await createNodeFileSystem().exists(path7)) {
30509
+ return { error: `${spec.label}: Content.metadata.${spec.key} does not exist: '${path7}'` };
30510
+ }
30511
+ return { path: path7 };
30512
+ }
30513
+ async function validatePublishInput(content) {
30514
+ const status = validatePublishStatus(content);
30515
+ if (status.error)
30516
+ return { error: status.error };
30235
30517
  const images = content.metadata?.images;
30236
30518
  if (!Array.isArray(images) || images.length === 0) {
30237
30519
  return { error: "images validation: metadata.images must be a non-empty ordered array" };
30238
30520
  }
30239
- const fs2 = createNodeFileSystem();
30521
+ const max = XHS_IMAGE_MAX;
30522
+ if (images.length > max) {
30523
+ return {
30524
+ error: `images validation: metadata.images holds ${images.length} entries but the \u4E0A\u4F20\u56FE\u6587 grid accepts \u2264${max}`
30525
+ };
30526
+ }
30240
30527
  const paths = [];
30241
30528
  for (const [i2, image] of images.entries()) {
30242
30529
  const path7 = image?.path;
@@ -30246,20 +30533,154 @@ async function validatePublishInput(content) {
30246
30533
  if (!isAbsolute(path7)) {
30247
30534
  return { error: `images validation: images[${i2}].path must be absolute, got '${path7}'` };
30248
30535
  }
30249
- if (!/\.(png|jpe?g|webp)$/i.test(path7)) {
30536
+ if (!IMAGE_EXTENSIONS.test(path7)) {
30250
30537
  return {
30251
- error: `images validation: images[${i2}].path has unsupported extension (expected png|jpg|jpeg|webp): '${path7}'`
30538
+ error: `images validation: images[${i2}].path has unsupported extension (expected ${IMAGE_LIST}): '${path7}'`
30252
30539
  };
30253
30540
  }
30254
- if (!await fs2.exists(path7)) {
30541
+ if (!await createNodeFileSystem().exists(path7)) {
30255
30542
  return { error: `images validation: images[${i2}].path does not exist: '${path7}'` };
30256
30543
  }
30257
30544
  paths.push(path7);
30258
30545
  }
30259
30546
  return { paths };
30260
30547
  }
30548
+ function validatePublishStatus(content) {
30549
+ const status = content.metadata?.publishStatus;
30550
+ if (status !== undefined && status !== "draft" && status !== "public") {
30551
+ return {
30552
+ error: `publishStatus validation: unknown publishStatus '${String(status)}' (expected 'draft' or 'public')`
30553
+ };
30554
+ }
30555
+ return {};
30556
+ }
30557
+ function validateTitle(content, operation) {
30558
+ const max = MODE_LIMITS[operation].titleMax;
30559
+ if (max === undefined)
30560
+ return {};
30561
+ const title = content.title ?? "";
30562
+ if (title.length > max) {
30563
+ return {
30564
+ error: `title validation: Content.title is ${title.length} chars but ${operation} accepts \u2264${max}`
30565
+ };
30566
+ }
30567
+ return {};
30568
+ }
30569
+ function validateBody(content, operation) {
30570
+ const max = MODE_LIMITS[operation].bodyMax;
30571
+ if (max === undefined)
30572
+ return {};
30573
+ if (content.body.length > max) {
30574
+ return {
30575
+ error: `body validation: Content.body is ${content.body.length} chars but ${operation} accepts \u2264${max}`
30576
+ };
30577
+ }
30578
+ return {};
30579
+ }
30580
+ async function validateModeInput(operation, content) {
30581
+ const status = validatePublishStatus(content);
30582
+ if (status.error)
30583
+ return { error: status.error };
30584
+ switch (operation) {
30585
+ case "\u4E0A\u4F20\u56FE\u6587": {
30586
+ const title = validateTitle(content, operation);
30587
+ if (title.error)
30588
+ return { error: title.error };
30589
+ const body = validateBody(content, operation);
30590
+ if (body.error)
30591
+ return { error: body.error };
30592
+ const images = await validatePublishInput(content);
30593
+ if (images.error)
30594
+ return { error: images.error };
30595
+ return { input: { operation, images: images.paths ?? [] } };
30596
+ }
30597
+ case "\u4E0A\u4F20\u89C6\u9891": {
30598
+ const title = validateTitle(content, operation);
30599
+ if (title.error)
30600
+ return { error: title.error };
30601
+ const video = await resolveMediaPath(content, {
30602
+ key: "video",
30603
+ label: "video validation",
30604
+ extensions: VIDEO_EXTENSIONS,
30605
+ list: VIDEO_LIST
30606
+ });
30607
+ if (video.error)
30608
+ return { error: video.error };
30609
+ return { input: { operation, video: video.path ?? "" } };
30610
+ }
30611
+ case "\u5199\u957F\u6587": {
30612
+ if (content.body.trim().length === 0) {
30613
+ return { error: "body validation: Content.body must be non-empty for \u5199\u957F\u6587 (authored in place)" };
30614
+ }
30615
+ return { input: { operation } };
30616
+ }
30617
+ case "\u53D1\u64AD\u5BA2": {
30618
+ const audio = await resolveMediaPath(content, {
30619
+ key: "audio",
30620
+ label: "audio validation",
30621
+ extensions: AUDIO_EXTENSIONS,
30622
+ list: AUDIO_LIST
30623
+ });
30624
+ if (audio.error)
30625
+ return { error: audio.error };
30626
+ const cover = await resolveMediaPath(content, {
30627
+ key: "coverImage",
30628
+ label: "cover validation",
30629
+ extensions: IMAGE_EXTENSIONS,
30630
+ list: IMAGE_LIST
30631
+ });
30632
+ if (cover.error)
30633
+ return { error: cover.error };
30634
+ return { input: { operation, audio: audio.path ?? "", cover: cover.path ?? "" } };
30635
+ }
30636
+ }
30637
+ }
30638
+
30639
+ // ../../plugins/publishings/xhs-pub/src/page.ts
30640
+ var POLL_MS = 500;
30641
+ var STEP_TIMEOUT_MS = 30000;
30642
+ var CREATOR_TAB_BAR = "div.creator-tab";
30643
+ var CREATOR_TAB_COUNT = 4;
30644
+ function creatorTabSelector(label) {
30645
+ return `${CREATOR_TAB_BAR}:has-text("${label}")`;
30646
+ }
30647
+ async function clickCreatorTab(page, label, opts = {}) {
30648
+ const timeoutMs = opts.timeoutMs ?? STEP_TIMEOUT_MS;
30649
+ const pollMs = opts.pollMs ?? POLL_MS;
30650
+ const onScreenCount = () => page.countOnScreen(CREATOR_TAB_BAR).catch(() => 0);
30651
+ const mounted = await waitUntil(() => onScreenCount().then((count2) => count2 >= CREATOR_TAB_COUNT), timeoutMs, pollMs);
30652
+ if (!mounted) {
30653
+ const observed = await onScreenCount();
30654
+ throw new Error(`mode entry: the console tab bar did not mount within ${timeoutMs}ms (${observed} of ${CREATOR_TAB_COUNT} on-screen '${CREATOR_TAB_BAR}' nodes) \u2014 every tab renders aria-hidden and off-viewport clones`);
30655
+ }
30656
+ if (!page.clickInViewport) {
30657
+ throw new Error(`mode entry: this driver cannot read viewport geometry, so the on-screen '${label}' tab cannot be told apart from its off-screen clones`);
30658
+ }
30659
+ const clicked = await page.clickInViewport(creatorTabSelector(label));
30660
+ if (!clicked) {
30661
+ throw new Error(`mode entry: no in-viewport '${label}' tab found (only aria-hidden or off-screen clones matched) \u2014 the mode was not entered`);
30662
+ }
30663
+ }
30261
30664
  function wrapPlaywrightPage(raw, shadow) {
30262
30665
  const pw = raw;
30666
+ const onScreen = async (selector) => {
30667
+ const viewport = pw.viewportSize?.() ?? null;
30668
+ if (viewport === null)
30669
+ return [];
30670
+ const total = await pw.locator(selector).count();
30671
+ const found = [];
30672
+ for (let index = 0;index < total; index += 1) {
30673
+ const candidate = pw.locator(selector).nth(index);
30674
+ const box = await candidate.boundingBox().catch(() => null);
30675
+ if (!box || box.width === 0 || box.height === 0)
30676
+ continue;
30677
+ const outside = box.x < 0 || box.y < 0 || box.x >= viewport.width || box.y >= viewport.height;
30678
+ if (outside)
30679
+ continue;
30680
+ found.push({ index, box });
30681
+ }
30682
+ return found;
30683
+ };
30263
30684
  return {
30264
30685
  goto: (url2, options) => pw.goto(url2, options),
30265
30686
  locator: (selector) => ({
@@ -30279,86 +30700,386 @@ function wrapPlaywrightPage(raw, shadow) {
30279
30700
  setInputFiles: (selector, paths) => pw.setInputFiles(selector, paths),
30280
30701
  waitForSelector: (selector, options) => pw.waitForSelector(selector, options),
30281
30702
  url: () => pw.url(),
30703
+ evaluate: (expression) => pw.evaluate(expression),
30704
+ countOnScreen: async (selector) => (await onScreen(selector)).length,
30282
30705
  clickInViewport: async (selector) => {
30283
- const viewport = pw.viewportSize?.() ?? null;
30284
- if (viewport === null)
30706
+ const [candidate] = await onScreen(selector);
30707
+ if (candidate === undefined)
30285
30708
  return false;
30286
- const total = await pw.locator(selector).count();
30287
- for (let index = 0;index < total; index += 1) {
30288
- const candidate = pw.locator(selector).nth(index);
30289
- const box = await candidate.boundingBox().catch(() => null);
30290
- if (!box || box.width === 0)
30291
- continue;
30292
- const outside = box.x < 0 || box.y < 0 || box.x >= viewport.width || box.y >= viewport.height;
30293
- if (outside)
30294
- continue;
30295
- await candidate.click({ force: true });
30296
- return true;
30297
- }
30298
- return false;
30709
+ await pw.locator(selector).nth(candidate.index).click({ force: true });
30710
+ return true;
30299
30711
  },
30300
30712
  findShadowControl: async (labels) => shadow ? shadow.findControl(labels) : null,
30301
30713
  clickAt: (x, y) => pw.mouse.click(x, y)
30302
30714
  };
30303
30715
  }
30304
- async function findControlViaCdp(session, labels) {
30305
- const document = await session.send("DOM.getDocument", { depth: -1, pierce: true });
30306
- if (!document.root)
30307
- return null;
30308
- const wanted = new Set(labels);
30309
- const text = (node) => node.nodeName === "#text" ? node.nodeValue ?? "" : (node.children ?? []).map(text).join("");
30310
- const hits = [];
30311
- const walk = (node) => {
30312
- if (hits.length > 0)
30313
- return;
30314
- for (const shadowRoot of node.shadowRoots ?? []) {
30315
- walk(shadowRoot);
30316
- if (hits.length > 0)
30317
- return;
30318
- }
30319
- if (node.nodeName === "BUTTON") {
30320
- const label = text(node).replace(/\s+/g, "");
30321
- if (wanted.has(label) && typeof node.backendNodeId === "number") {
30322
- hits.push({ label, backendNodeId: node.backendNodeId });
30323
- return;
30324
- }
30716
+ async function findSelector(page, selectors, what, timeoutMs = STEP_TIMEOUT_MS) {
30717
+ const deadline = Date.now() + timeoutMs;
30718
+ while (Date.now() < deadline) {
30719
+ const found = await trySelectors(page, selectors);
30720
+ if (found.found && found.selector)
30721
+ return found.selector;
30722
+ await new Promise((resolve4) => setTimeout(resolve4, POLL_MS));
30723
+ }
30724
+ throw new Error(`${what}: no matching element observed`);
30725
+ }
30726
+ async function findAttachedSelector(page, selectors, what, timeoutMs = STEP_TIMEOUT_MS) {
30727
+ const deadline = Date.now() + timeoutMs;
30728
+ while (Date.now() < deadline) {
30729
+ for (const selector of selectors) {
30730
+ const attached = await page.locator(selector).count().catch(() => 0);
30731
+ if (attached > 0)
30732
+ return selector;
30325
30733
  }
30326
- for (const child of node.children ?? []) {
30327
- walk(child);
30328
- if (hits.length > 0)
30329
- return;
30734
+ await new Promise((resolve4) => setTimeout(resolve4, POLL_MS));
30735
+ }
30736
+ throw new Error(`${what}: no matching element observed`);
30737
+ }
30738
+
30739
+ // ../../plugins/publishings/xhs-pub/src/flow.ts
30740
+ var NOTE_TITLE = [
30741
+ 'input.d-text[placeholder="\u586B\u5199\u6807\u9898\u4F1A\u6709\u66F4\u591A\u8D5E\u54E6"]',
30742
+ 'input[placeholder*="\u6807\u9898"]',
30743
+ "#title-textarea input"
30744
+ ];
30745
+ var NOTE_BODY = [
30746
+ 'div.tiptap[contenteditable="true"]',
30747
+ 'div[contenteditable="true"].ql-editor',
30748
+ "#post-textarea",
30749
+ ".ql-editor"
30750
+ ];
30751
+ var NOTE_THUMBNAIL = ["img.preivew-image", ".img-preview img"];
30752
+ var IMAGE_FILE_INPUT = [
30753
+ 'input[type="file"][accept*=".jpg"]',
30754
+ 'input[type="file"].upload-input',
30755
+ 'input[type="file"]'
30756
+ ];
30757
+ var VIDEO_FILE_INPUT = ['input[type="file"][accept*=".mp4"]', 'input[type="file"].upload-input'];
30758
+ var VIDEO_EDITOR = ["div.tiptap.ProseMirror", 'div.tiptap[contenteditable="true"]', ".ProseMirror"];
30759
+ var ARTICLE_NEW = ["text=\u65B0\u7684\u521B\u4F5C", 'button:has-text("\u65B0\u7684\u521B\u4F5C")'];
30760
+ var ARTICLE_EDITOR = ["div.tiptap.ProseMirror.ProseMirror-focused", "div.tiptap.ProseMirror", ".ProseMirror"];
30761
+ var ARTICLE_LAYOUT = ['button:has-text("\u4E00\u952E\u6392\u7248")', "div.ailayout-btn_wrp", "div.ailayout-btn_label"];
30762
+ var PODCAST_OPEN = ["text=\u4E0A\u4F20\u97F3\u9891", 'button:has-text("\u4E0A\u4F20\u97F3\u9891")'];
30763
+ var PODCAST_MODAL = ["div.creator-modal-sty", ".d-modal.d-modal-centered", ".d-modal"];
30764
+ var PODCAST_AUDIO_INPUT = ['input[type="file"][accept*=".mp3"]', 'input[type="file"][accept*=".wav"]'];
30765
+ var PODCAST_COVER_INPUT = ['input[type="file"][accept*=".jpg"]', 'input[type="file"][accept*=".png"]'];
30766
+ var PODCAST_MODAL_ADVANCE = ["\u53BB\u53D1\u5E03"];
30767
+ var DRAFT_LABELS = ["\u6682\u5B58\u79BB\u5F00", "\u5B58\u8349\u7A3F", "\u4FDD\u5B58\u8349\u7A3F"];
30768
+ var PUBLISH_LABELS = ["\u53D1\u5E03"];
30769
+ var PUBLISH_CONFIRM = ["\u53D1\u5E03\u6210\u529F", "\u5DF2\u53D1\u5E03"];
30770
+ var DRAFT_TOASTS = ["\u4FDD\u5B58\u6210\u529F"];
30771
+ var SHADOW_BAR = "the xhs publish bar (closed shadow root)";
30772
+ var MODE_ACTION_PATHS = {
30773
+ \u{4e0a}\u{4f20}\u{56fe}\u{6587}: ["shadow"],
30774
+ \u{4e0a}\u{4f20}\u{89c6}\u{9891}: ["shadow"],
30775
+ \u{5199}\u{957f}\u{6587}: ["light", "shadow"],
30776
+ \u{53d1}\u{64ad}\u{5ba2}: ["shadow"]
30777
+ };
30778
+ function fail(error51) {
30779
+ return ResultSchema.parse({ ok: false, target: TARGET, error: error51 });
30780
+ }
30781
+ function sleep2(ms) {
30782
+ return new Promise((resolve4) => setTimeout(resolve4, ms));
30783
+ }
30784
+ function asPlaywrightPage(page) {
30785
+ return page;
30786
+ }
30787
+ async function enterMode(deps, label) {
30788
+ await clickCreatorTab(deps.page, label, { timeoutMs: deps.stepTimeoutMs });
30789
+ }
30790
+ async function clickShadowControl(page, labels) {
30791
+ if (!page.findShadowControl || !page.clickAt) {
30792
+ return {
30793
+ ok: false,
30794
+ error: `submit control: ${SHADOW_BAR} and this driver has no CDP access to resolve it`
30795
+ };
30796
+ }
30797
+ const control = await page.findShadowControl(labels);
30798
+ if (!control) {
30799
+ return {
30800
+ ok: false,
30801
+ error: `submit control: none of [${labels.join(", ")}] found in ${SHADOW_BAR}`
30802
+ };
30803
+ }
30804
+ await page.clickAt(control.x, control.y);
30805
+ return { ok: true, label: control.label, x: control.x, y: control.y };
30806
+ }
30807
+ async function clickLightControl(page, labels, timeoutMs, what) {
30808
+ const candidates = labels.flatMap((label2) => [`button:has-text("${label2}")`, `text=${label2}`]);
30809
+ let selector;
30810
+ try {
30811
+ selector = await findSelector(page, candidates, `${what} (light DOM) none of [${labels.join(", ")}]`, timeoutMs);
30812
+ } catch (error51) {
30813
+ return { ok: false, error: error51 instanceof Error ? error51.message : String(error51) };
30814
+ }
30815
+ const label = labels.find((candidate) => selector.includes(candidate)) ?? labels[0];
30816
+ await page.locator(selector).first().click({ timeout: timeoutMs });
30817
+ return { ok: true, label: label ?? "", selector };
30818
+ }
30819
+ async function clickActionControl(operation, deps, labels, what) {
30820
+ let lastError = "";
30821
+ for (const path7 of MODE_ACTION_PATHS[operation]) {
30822
+ if (path7 === "shadow") {
30823
+ const clicked2 = await clickShadowControl(deps.page, labels);
30824
+ if (clicked2.ok)
30825
+ return { ok: true, path: path7, label: clicked2.label, click: `${clicked2.x},${clicked2.y}` };
30826
+ lastError = clicked2.error;
30827
+ continue;
30330
30828
  }
30331
- };
30332
- walk(document.root);
30333
- const hit = hits[0];
30334
- if (!hit)
30335
- return null;
30336
- const box = await session.send("DOM.getBoxModel", { backendNodeId: hit.backendNodeId });
30337
- const quad = box.model?.content;
30338
- if (!quad || quad.length < 8)
30339
- return null;
30340
- const [x1 = 0, y1 = 0, x3 = 0, , , y4 = 0] = quad;
30341
- return {
30342
- label: hit.label,
30343
- x: Math.round((x1 + x3) / 2),
30344
- y: Math.round((y1 + y4) / 2)
30345
- };
30829
+ const clicked = await clickLightControl(deps.page, labels, deps.stepTimeoutMs, what);
30830
+ if (clicked.ok)
30831
+ return { ok: true, path: path7, label: clicked.label, click: `selector:${clicked.selector}` };
30832
+ lastError = clicked.error;
30833
+ }
30834
+ return { ok: false, error: lastError };
30346
30835
  }
30347
- async function createShadowProbe(context2, page) {
30348
- if (typeof context2.newCDPSession !== "function")
30836
+ async function readDraftCount(page) {
30837
+ for (const selector of ["text=/\u8349\u7A3F\u7BB1\\(\\d+\\)/", "text=\u8349\u7A3F\u7BB1"]) {
30838
+ const attached = await page.locator(selector).count().catch(() => 0);
30839
+ if (attached === 0)
30840
+ continue;
30841
+ const locator = page.locator(selector).first();
30842
+ if (!locator.textContent)
30843
+ continue;
30844
+ const text = await locator.textContent().catch(() => "");
30845
+ const match = /\u8349\u7A3F\u7BB1\s*[\uFF08(]\s*(\d+)\s*[\uFF09)]/.exec(text);
30846
+ if (match?.[1] !== undefined)
30847
+ return Number(match[1]);
30848
+ }
30849
+ return null;
30850
+ }
30851
+ function draftSavedStamps(now = new Date) {
30852
+ const pad = (value) => String(value).padStart(2, "0");
30853
+ return [0, 1, 2].map((minutesAgo) => {
30854
+ const stamp = new Date(now.getTime() - minutesAgo * 60000);
30855
+ return `\u4FDD\u5B58\u4E8E${stamp.getFullYear()}-${pad(stamp.getMonth() + 1)}-${pad(stamp.getDate())} ${pad(stamp.getHours())}:${pad(stamp.getMinutes())}`;
30856
+ });
30857
+ }
30858
+ async function readDraftStamps(page) {
30859
+ const present = [];
30860
+ for (const stamp of draftSavedStamps()) {
30861
+ const attached = await page.locator(`text=${stamp}`).count().catch(() => 0);
30862
+ if (attached > 0)
30863
+ present.push(stamp);
30864
+ }
30865
+ return present;
30866
+ }
30867
+ async function readDraftBaseline(page) {
30868
+ return { count: await readDraftCount(page), stamps: await readDraftStamps(page) };
30869
+ }
30870
+ async function isDraftRowSaved(page, baseline) {
30871
+ const stamps = await readDraftStamps(page);
30872
+ if (stamps.some((stamp) => !baseline.stamps.includes(stamp)))
30873
+ return true;
30874
+ const after = await readDraftCount(page);
30875
+ if (baseline.count === null || after === null)
30876
+ return false;
30877
+ return after > baseline.count;
30878
+ }
30879
+ async function draftSignalEvidence(page, baseline) {
30880
+ const toast = await readScopedSignal(asPlaywrightPage(page), DRAFT_TOASTS);
30881
+ if (toast !== null)
30882
+ return `toast:${toast}`;
30883
+ if (await isDraftRowSaved(page, baseline))
30884
+ return "draft-row";
30885
+ return null;
30886
+ }
30887
+ async function applyNoteTitle(deps) {
30888
+ const selector = await findSelector(deps.page, NOTE_TITLE, "title input", deps.stepTimeoutMs);
30889
+ await deps.page.locator(selector).first().fill(deps.content.title ?? "");
30890
+ }
30891
+ async function typeNoteBody(deps, selectors, what) {
30892
+ const selector = await findSelector(deps.page, selectors, what, deps.stepTimeoutMs);
30893
+ const body = deps.page.locator(selector).first();
30894
+ const text = deps.content.body ?? "";
30895
+ if (typeof body.type === "function") {
30896
+ await body.click();
30897
+ await body.type(text, { delay: 5 });
30349
30898
  return;
30899
+ }
30900
+ await body.fill(text);
30901
+ }
30902
+ async function confirmThumbnails(deps, expected) {
30903
+ const selector = await findSelector(deps.page, NOTE_THUMBNAIL, "thumbnail wait", deps.stepTimeoutMs);
30904
+ const matches = await waitUntil(() => deps.page.locator(selector).count().then((count2) => count2 >= expected), deps.stepTimeoutMs);
30905
+ const observed = await deps.page.locator(selector).count();
30906
+ if (!matches || observed !== expected) {
30907
+ throw new Error(`thumbnail count: expected ${expected}, observed ${observed}`);
30908
+ }
30909
+ }
30910
+ async function composeImageText(input, deps) {
30911
+ await enterMode(deps, input.operation);
30912
+ const fileSelector = await findAttachedSelector(deps.page, IMAGE_FILE_INPUT, "image file input", deps.stepTimeoutMs);
30913
+ await deps.page.setInputFiles(fileSelector, input.images);
30914
+ await confirmThumbnails(deps, input.images.length);
30915
+ await applyNoteTitle(deps);
30916
+ await typeNoteBody(deps, NOTE_BODY, "note body");
30917
+ }
30918
+ async function composeVideo(input, deps) {
30919
+ await enterMode(deps, input.operation);
30920
+ const fileSelector = await findAttachedSelector(deps.page, VIDEO_FILE_INPUT, "video file input", deps.stepTimeoutMs);
30921
+ await uploadFile(asPlaywrightPage(deps.page), fileSelector, input.video, { timeoutMs: deps.stepTimeoutMs });
30922
+ await findSelector(deps.page, VIDEO_EDITOR, "video note editor", deps.stepTimeoutMs);
30923
+ await applyNoteTitle(deps);
30924
+ await typeNoteBody(deps, NOTE_BODY, "note body");
30925
+ }
30926
+ async function composeArticle(deps) {
30927
+ await enterMode(deps, "\u5199\u957F\u6587");
30928
+ const newArticle = await findAttachedSelector(deps.page, ARTICLE_NEW, "\u65B0\u7684\u521B\u4F5C control", deps.stepTimeoutMs);
30929
+ await deps.page.locator(newArticle).first().click();
30930
+ const editor = await findSelector(deps.page, ARTICLE_EDITOR, "long-article editor", deps.stepTimeoutMs);
30931
+ await pasteHtml(asPlaywrightPage(deps.page), editor, markdownToHtml(deps.content.body), {
30932
+ timeoutMs: deps.stepTimeoutMs
30933
+ });
30350
30934
  try {
30351
- const session = await context2.newCDPSession(page);
30352
- await session.send("DOM.enable");
30353
- return { findControl: (labels) => findControlViaCdp(session, labels) };
30354
- } catch {
30935
+ const layout = await findSelector(deps.page, ARTICLE_LAYOUT, "\u4E00\u952E\u6392\u7248 control", deps.stepTimeoutMs);
30936
+ await deps.page.locator(layout).first().click();
30937
+ } catch {}
30938
+ }
30939
+ async function isPodcastModalOpen(page) {
30940
+ return trySelectors(page, PODCAST_MODAL).then((found) => found.found).catch(() => false);
30941
+ }
30942
+ async function leavePodcastModal(deps) {
30943
+ const closed = () => isPodcastModalOpen(deps.page).then((open) => !open);
30944
+ if (await waitUntil(closed, deps.stepTimeoutMs))
30355
30945
  return;
30946
+ if (deps.publishStatus === "public") {
30947
+ const advance = await clickLightControl(deps.page, PODCAST_MODAL_ADVANCE, deps.stepTimeoutMs, "podcast modal advance");
30948
+ if (!advance.ok)
30949
+ throw new Error(advance.error);
30950
+ if (await waitUntil(closed, deps.stepTimeoutMs))
30951
+ return;
30952
+ }
30953
+ throw new Error(`podcast modal: the \u53D1\u64AD\u5BA2 modal was still open ${deps.stepTimeoutMs}ms after the audio and cover were attached; the only recorded advance control is '${PODCAST_MODAL_ADVANCE[0]}' and a draft run does not click it (it is publish-adjacent)`);
30954
+ }
30955
+ async function composePodcast(input, deps) {
30956
+ await enterMode(deps, input.operation);
30957
+ const open = await findAttachedSelector(deps.page, PODCAST_OPEN, "\u4E0A\u4F20\u97F3\u9891 control", deps.stepTimeoutMs);
30958
+ await deps.page.locator(open).first().click();
30959
+ await findSelector(deps.page, PODCAST_MODAL, "podcast modal", deps.stepTimeoutMs);
30960
+ const audioInput = await findAttachedSelector(deps.page, PODCAST_AUDIO_INPUT, "podcast audio input", deps.stepTimeoutMs);
30961
+ await uploadFile(asPlaywrightPage(deps.page), audioInput, input.audio, { timeoutMs: deps.stepTimeoutMs });
30962
+ const coverInput = await findAttachedSelector(deps.page, PODCAST_COVER_INPUT, "podcast cover input", deps.stepTimeoutMs);
30963
+ await uploadFile(asPlaywrightPage(deps.page), coverInput, input.cover, { timeoutMs: deps.stepTimeoutMs });
30964
+ await leavePodcastModal(deps);
30965
+ }
30966
+ async function compose(input, deps) {
30967
+ switch (input.operation) {
30968
+ case "\u4E0A\u4F20\u56FE\u6587":
30969
+ await composeImageText(input, deps);
30970
+ return;
30971
+ case "\u4E0A\u4F20\u89C6\u9891":
30972
+ await composeVideo(input, deps);
30973
+ return;
30974
+ case "\u5199\u957F\u6587":
30975
+ await composeArticle(deps);
30976
+ return;
30977
+ case "\u53D1\u64AD\u5BA2":
30978
+ await composePodcast(input, deps);
30979
+ return;
30356
30980
  }
30357
30981
  }
30982
+ function baseMetadata(input, deps) {
30983
+ return {
30984
+ operation: input.operation,
30985
+ driver: deps.driver,
30986
+ publishStatus: deps.publishStatus,
30987
+ profileDir: deps.profileDir,
30988
+ artifactPaths: artifactPaths(input),
30989
+ ...input.operation === "\u4E0A\u4F20\u56FE\u6587" ? { imageCount: input.images.length } : {}
30990
+ };
30991
+ }
30992
+ async function saveDraft(input, deps) {
30993
+ let clicked = null;
30994
+ let signal = null;
30995
+ let attempts = 0;
30996
+ while (signal === null && attempts < deps.submitAttempts) {
30997
+ attempts += 1;
30998
+ await sleep2(deps.submitSettleMs);
30999
+ const control = await clickActionControl(input.operation, deps, DRAFT_LABELS, "draft control");
31000
+ if (!control.ok)
31001
+ return fail(control.error);
31002
+ clicked = control;
31003
+ const outcome = await expectOutcome(() => draftSignalEvidence(deps.page, deps.draftBaseline), {
31004
+ timeoutMs: deps.submitSettleMs,
31005
+ label: `draft confirmation '${control.label}'`,
31006
+ pollMs: POLL_MS
31007
+ });
31008
+ if (outcome.ok)
31009
+ signal = outcome.evidence;
31010
+ }
31011
+ const draftCountAfter = await readDraftCount(deps.page);
31012
+ if (signal === null || clicked === null) {
31013
+ return fail(`draft confirmation: '${clicked?.label ?? DRAFT_LABELS[0]}' was clicked ${attempts}\xD7 but neither a fresh ${TARGET} save timestamp, a moved draft counter, nor a scoped '${DRAFT_TOASTS[0]}' toast was observed within ${deps.submitSettleMs}ms (before=${String(deps.draftBaseline.count)}, after=${String(draftCountAfter)})`);
31014
+ }
31015
+ return ResultSchema.parse({
31016
+ ok: true,
31017
+ target: TARGET,
31018
+ message: `${TARGET} draft saved (unpublished) in profile ${deps.profileDir} via '${clicked.label}' (${input.operation})`,
31019
+ metadata: {
31020
+ ...baseMetadata(input, deps),
31021
+ draftControl: clicked.label,
31022
+ draftClick: clicked.click,
31023
+ draftPath: clicked.path,
31024
+ draftAttempts: attempts,
31025
+ draftSignal: signal,
31026
+ draftCountBefore: deps.draftBaseline.count,
31027
+ draftCountAfter
31028
+ }
31029
+ });
31030
+ }
31031
+ async function publishNote(input, deps) {
31032
+ await sleep2(deps.submitSettleMs);
31033
+ const submitted = await clickActionControl(input.operation, deps, PUBLISH_LABELS, "publish control");
31034
+ if (!submitted.ok)
31035
+ return fail(submitted.error);
31036
+ const urlBeforeSubmit = deps.page.url();
31037
+ const confirmed = await waitUntil(async () => {
31038
+ if (deps.page.url().includes(PUBLISHED_MARKER))
31039
+ return true;
31040
+ return await readScopedSignal(asPlaywrightPage(deps.page), PUBLISH_CONFIRM) !== null;
31041
+ }, deps.stepTimeoutMs);
31042
+ if (!confirmed) {
31043
+ return fail(`publish submission: state unknown \u2014 the '${submitted.label}' click (${submitted.click}) was sent from ${urlBeforeSubmit} but no success state was observed (now ${deps.page.url()}). Check the ${TARGET} creator note manager before retrying: a retry could duplicate the post.`);
31044
+ }
31045
+ const url2 = deps.page.url();
31046
+ return ResultSchema.parse({
31047
+ ok: true,
31048
+ target: TARGET,
31049
+ ...url2 !== PUBLISH_URL ? { url: url2 } : {},
31050
+ message: `${TARGET} ${input.operation} post published`,
31051
+ metadata: {
31052
+ ...baseMetadata(input, deps),
31053
+ publishControl: submitted.label,
31054
+ publishPath: submitted.path,
31055
+ publishClick: submitted.click,
31056
+ publishSignal: url2.includes(PUBLISHED_MARKER) ? `url:${PUBLISHED_MARKER}` : "toast"
31057
+ }
31058
+ });
31059
+ }
31060
+ async function runMode(input, deps) {
31061
+ await compose(input, deps);
31062
+ return deps.publishStatus === "public" ? publishNote(input, deps) : saveDraft(input, deps);
31063
+ }
31064
+
31065
+ // ../../plugins/publishings/xhs-pub/src/index.ts
31066
+ var CHANNEL = "xhs";
31067
+ var LOGIN_TIMEOUT_MS = 120000;
31068
+ var PREFLIGHT_TIMEOUT_MS = 30000;
31069
+ var SUBMIT_SETTLE_MS = 2500;
31070
+ var SUBMIT_ATTEMPTS = 3;
31071
+ var AUTH_GATE = ["div.creator-tab", "text=\u4E0A\u4F20\u56FE\u6587"];
31072
+ function fail2(error51) {
31073
+ return ResultSchema.parse({ ok: false, target: TARGET, error: error51 });
31074
+ }
31075
+ async function createShadowProbe(context2, page) {
31076
+ const find = await createShadowFinder(context2, page);
31077
+ return find === undefined ? undefined : { findControl: (labels) => find(labels) };
31078
+ }
30358
31079
  async function isXhsConsoleReady(page) {
30359
- return trySelectors(page, SELECTORS.imageTab).then((r) => r.found);
31080
+ return trySelectors(page, AUTH_GATE).then((r) => r.found);
30360
31081
  }
30361
- var defaultLauncher2 = openPersistentBrowser;
31082
+ var defaultLauncher2 = kkBrowserLauncher.launch;
30362
31083
  function makeDefaultOpenPage(launch = defaultLauncher2) {
30363
31084
  return async (profileDir) => {
30364
31085
  let context2;
@@ -30379,67 +31100,17 @@ function makeDefaultOpenPage(launch = defaultLauncher2) {
30379
31100
  };
30380
31101
  }
30381
31102
  var defaultOpenPage = makeDefaultOpenPage();
30382
- async function waitUntil(predicate, timeoutMs) {
30383
- const deadline = Date.now() + timeoutMs;
30384
- while (Date.now() < deadline) {
30385
- if (await predicate())
30386
- return true;
30387
- await new Promise((resolve3) => setTimeout(resolve3, POLL_MS));
30388
- }
30389
- return predicate();
30390
- }
30391
- async function findSelector(page, selectors, what, timeoutMs = STEP_TIMEOUT_MS) {
30392
- const deadline = Date.now() + timeoutMs;
30393
- while (Date.now() < deadline) {
30394
- const found = await trySelectors(page, selectors);
30395
- if (found.found && found.selector)
30396
- return found.selector;
30397
- await new Promise((resolve3) => setTimeout(resolve3, POLL_MS));
30398
- }
30399
- throw new Error(`${what}: no matching element observed`);
30400
- }
30401
- async function findAttachedSelector(page, selectors, what, timeoutMs = STEP_TIMEOUT_MS) {
30402
- const deadline = Date.now() + timeoutMs;
30403
- while (Date.now() < deadline) {
30404
- for (const selector of selectors) {
30405
- const attached = await page.locator(selector).count().catch(() => 0);
30406
- if (attached > 0)
30407
- return selector;
30408
- }
30409
- await new Promise((resolve3) => setTimeout(resolve3, POLL_MS));
30410
- }
30411
- throw new Error(`${what}: no matching element observed`);
30412
- }
30413
31103
  async function checkXhsProfileSession(opts) {
30414
- const timeoutMs = opts.timeoutMs ?? PREFLIGHT_TIMEOUT_MS;
30415
- for (const headless of [true, false]) {
30416
- try {
30417
- await withProfilePage({
30418
- profileDir: opts.profileDir,
30419
- headless,
30420
- url: PUBLISH_URL,
30421
- isAuthenticated: (page) => isXhsConsoleReady(wrapPlaywrightPage(page)),
30422
- timeoutMs,
30423
- launcher: opts.launcher
30424
- }, async () => {
30425
- return;
30426
- });
30427
- return {
30428
- status: "verified",
30429
- profileDir: opts.profileDir,
30430
- detail: `authenticated ${CHANNEL} creator console observed ${headless ? "headlessly" : "via headed fallback"} via profile ${opts.profileDir} (read-only)`
30431
- };
30432
- } catch (error51) {
30433
- if (headless)
30434
- continue;
30435
- return {
30436
- status: "refused",
30437
- profileDir: opts.profileDir,
30438
- detail: `headless and headed readiness refused: ${error51 instanceof Error ? error51.message : String(error51)}`
30439
- };
30440
- }
30441
- }
30442
- throw new Error("unreachable: session check loop must return or throw");
31104
+ return checkChannelSession({
31105
+ channel: CHANNEL,
31106
+ homeUrl: PUBLISH_URL,
31107
+ isAuthenticated: (page) => isXhsConsoleReady(wrapPlaywrightPage(page))
31108
+ }, {
31109
+ profileDir: opts.profileDir,
31110
+ launcher: opts.launcher,
31111
+ timeoutMs: opts.timeoutMs ?? PREFLIGHT_TIMEOUT_MS,
31112
+ headedOnly: opts.headedOnly === true
31113
+ });
30443
31114
  }
30444
31115
 
30445
31116
  class XhsBrowserTransport {
@@ -30462,91 +31133,34 @@ class XhsBrowserTransport {
30462
31133
  this.automation = opts?.automation ?? createAutomationGuard();
30463
31134
  this.preflight = opts?.preflight ?? ((profileDir) => checkXhsProfileSession({ profileDir }));
30464
31135
  }
30465
- async readDraftCount(page) {
30466
- for (const selector of SELECTORS.draftBox) {
30467
- const attached = await page.locator(selector).count().catch(() => 0);
30468
- if (attached === 0)
30469
- continue;
30470
- const locator = page.locator(selector).first();
30471
- if (!locator.textContent)
30472
- continue;
30473
- const text = await locator.textContent().catch(() => "");
30474
- const match = /\u8349\u7A3F\u7BB1\s*[\uFF08(]\s*(\d+)\s*[\uFF09)]/.exec(text);
30475
- if (match?.[1] !== undefined)
30476
- return Number(match[1]);
30477
- }
30478
- return null;
30479
- }
30480
- static draftSavedStamps(now = new Date) {
30481
- const pad = (value) => String(value).padStart(2, "0");
30482
- return [0, 1, 2].map((minutesAgo) => {
30483
- const stamp = new Date(now.getTime() - minutesAgo * 60000);
30484
- return `\u4FDD\u5B58\u4E8E${stamp.getFullYear()}-${pad(stamp.getMonth() + 1)}-${pad(stamp.getDate())} ${pad(stamp.getHours())}:${pad(stamp.getMinutes())}`;
30485
- });
30486
- }
30487
- async readDraftStamps(page) {
30488
- const present = [];
30489
- for (const stamp of XhsBrowserTransport.draftSavedStamps()) {
30490
- const attached = await page.locator(`text=${stamp}`).count().catch(() => 0);
30491
- if (attached > 0)
30492
- present.push(stamp);
30493
- }
30494
- return present;
30495
- }
30496
- async readDraftBaseline(page) {
30497
- return { count: await this.readDraftCount(page), stamps: await this.readDraftStamps(page) };
30498
- }
30499
- async isDraftSaved(page, baseline) {
30500
- const stamps = await this.readDraftStamps(page);
30501
- if (stamps.some((stamp) => !baseline.stamps.includes(stamp)))
30502
- return true;
30503
- const after = await this.readDraftCount(page);
30504
- if (baseline.count === null || after === null)
30505
- return false;
30506
- return after > baseline.count;
30507
- }
30508
- async clickShadowControl(page, labels) {
30509
- if (!page.findShadowControl || !page.clickAt) {
30510
- return {
30511
- ok: false,
30512
- error: `submit control: the ${CHANNEL} publish bar lives in a closed shadow root and this driver has no CDP access to resolve it`
30513
- };
30514
- }
30515
- const control = await page.findShadowControl(labels);
30516
- if (!control) {
30517
- return {
30518
- ok: false,
30519
- error: `submit control: none of [${labels.join(", ")}] found in the ${CHANNEL} publish bar (closed shadow root)`
30520
- };
30521
- }
30522
- await page.clickAt(control.x, control.y);
30523
- return { ok: true, label: control.label, x: control.x, y: control.y };
30524
- }
30525
31136
  async publish(payload) {
30526
31137
  if (this.driver !== "script") {
30527
- return fail(`unsupported driver '${this.driver}' for ${TARGET}: image posts require the script driver (Playwright setInputFiles)`);
30528
- }
30529
- const override = typeof payload.options?.profileDir === "string" ? payload.options.profileDir : process.env.XHS_PROFILE_DIR ?? process.env.KK_BROWSER_PROFILE_DIR;
30530
- const profileDir = resolveBrowserProfileDir(CHANNEL, override);
30531
- const validation = await validatePublishInput(payload.content);
30532
- if (validation.error)
30533
- return fail(validation.error);
30534
- const paths = validation.paths ?? [];
31138
+ return fail2(`unsupported driver '${this.driver}' for ${TARGET}: the console modes (${XHS_OPERATIONS.join(", ")}) require the script driver (Playwright setInputFiles)`);
31139
+ }
31140
+ const requested = resolveXhsOperation(payload.options, payload.content);
31141
+ if (requested.error !== undefined)
31142
+ return fail2(requested.error);
31143
+ const operation = requested.operation;
31144
+ const validation = await validateModeInput(operation, payload.content);
31145
+ if (validation.error !== undefined)
31146
+ return fail2(validation.error);
31147
+ const input = validation.input;
31148
+ const override = typeof payload.options?.profileDir === "string" ? payload.options.profileDir : undefined;
31149
+ const profileDir = resolveChannelProfileDir(CHANNEL, override);
30535
31150
  const publishStatus = payload.content.metadata?.publishStatus === "public" ? "public" : "draft";
30536
- const base2 = { driver: this.driver, publishStatus, profileDir, imageCount: paths.length };
30537
31151
  const cap = this.automation.allow("publish");
30538
31152
  if (!cap.allowed)
30539
- return fail(`automation cap: ${cap.reason}`);
31153
+ return fail2(`automation cap: ${cap.reason}`);
30540
31154
  const session = await this.preflight(profileDir);
30541
31155
  if (session.status !== "verified") {
30542
- return fail(`session preflight refused (no headed visit attempted): ${session.detail}`);
31156
+ return fail2(`session preflight refused (no headed visit attempted): ${session.detail}`);
30543
31157
  }
30544
31158
  this.automation.record("publish");
30545
31159
  let opened;
30546
31160
  try {
30547
31161
  opened = await this.openPage(profileDir);
30548
31162
  } catch (error51) {
30549
- return fail(error51 instanceof Error ? error51.message : String(error51));
31163
+ return fail2(error51 instanceof Error ? error51.message : String(error51));
30550
31164
  }
30551
31165
  const { page, close } = opened;
30552
31166
  const act = async (what, fn) => {
@@ -30560,99 +31174,22 @@ class XhsBrowserTransport {
30560
31174
  await act("creator page goto", () => page.goto(PUBLISH_URL, { timeout: this.loginTimeoutMs }));
30561
31175
  const gated = await waitUntil(() => isXhsConsoleReady(page), this.loginTimeoutMs);
30562
31176
  if (!gated) {
30563
- return fail(`login timeout after ${this.loginTimeoutMs}ms: no authenticated ${CHANNEL} creator console in ${profileDir}`);
30564
- }
30565
- const step = (selectors, what) => findSelector(page, selectors, what, this.stepTimeoutMs);
30566
- const draftBaseline = publishStatus === "draft" ? await this.readDraftBaseline(page) : { count: null, stamps: [] };
30567
- const tabSelector = await step(SELECTORS.imageTab, "image-post tab");
30568
- const clickInViewport = page.clickInViewport;
30569
- if (clickInViewport) {
30570
- const switched = await act("image-post tab click", () => clickInViewport.call(page, tabSelector));
30571
- if (!switched)
30572
- return fail("image-post tab click: no in-viewport image tab found");
30573
- } else {
30574
- await act("image-post tab click", () => page.locator(tabSelector).first().click({ force: true }));
30575
- }
30576
- const fileSelector = await findAttachedSelector(page, SELECTORS.fileInput, "image file input", this.stepTimeoutMs);
30577
- await act("image upload", () => page.setInputFiles(fileSelector, paths));
30578
- const thumbSelector = await step(SELECTORS.thumbnail, "thumbnail wait");
30579
- const countMatches = await waitUntil(() => act("thumbnail count read", () => page.locator(thumbSelector).count()).then((n2) => n2 >= paths.length), this.stepTimeoutMs);
30580
- const thumbCount = await act("thumbnail count read", () => page.locator(thumbSelector).count());
30581
- if (!countMatches || thumbCount !== paths.length) {
30582
- return fail(`thumbnail count: expected ${paths.length}, observed ${thumbCount}`);
30583
- }
30584
- const titleSelector = await step(SELECTORS.title, "title input");
30585
- await act("title fill", () => page.locator(titleSelector).first().fill(payload.content.title ?? ""));
30586
- const bodySelector = await step(SELECTORS.body, "note body");
30587
- await act("body fill", async () => {
30588
- const body = page.locator(bodySelector).first();
30589
- const text = payload.content.body ?? "";
30590
- if (typeof body.type === "function") {
30591
- await body.click();
30592
- await body.type(text, { delay: 5 });
30593
- return;
30594
- }
30595
- await body.fill(text);
30596
- });
30597
- if (publishStatus === "draft") {
30598
- let submitted2 = null;
30599
- let confirmed2 = false;
30600
- let attempts = 0;
30601
- while (!confirmed2 && attempts < this.submitAttempts) {
30602
- attempts += 1;
30603
- await new Promise((resolve3) => setTimeout(resolve3, this.submitSettleMs));
30604
- const clicked = await this.clickShadowControl(page, SHADOW_CONTROLS.draft);
30605
- if (!clicked.ok)
30606
- return fail(clicked.error);
30607
- submitted2 = clicked;
30608
- confirmed2 = await waitUntil(() => this.isDraftSaved(page, draftBaseline), this.submitSettleMs);
30609
- }
30610
- const draftCountAfter = await this.readDraftCount(page);
30611
- if (!confirmed2 || submitted2 === null) {
30612
- return fail(`draft confirmation: '${submitted2?.label ?? SHADOW_CONTROLS.draft[0]}' was clicked ${attempts}\xD7 but neither a fresh ${CHANNEL} save timestamp nor a moved draft counter was observed (before=${String(draftBaseline.count)}, after=${String(draftCountAfter)})`);
30613
- }
30614
- return ResultSchema.parse({
30615
- ok: true,
30616
- target: TARGET,
30617
- message: `${TARGET} draft saved (unpublished) in profile ${profileDir} via '${submitted2.label}'`,
30618
- metadata: {
30619
- ...base2,
30620
- draftControl: submitted2.label,
30621
- draftClick: `${submitted2.x},${submitted2.y}`,
30622
- draftAttempts: attempts,
30623
- draftCountBefore: draftBaseline.count,
30624
- draftCountAfter
30625
- }
30626
- });
30627
- }
30628
- await new Promise((resolve3) => setTimeout(resolve3, this.submitSettleMs));
30629
- const submitted = await this.clickShadowControl(page, SHADOW_CONTROLS.publish);
30630
- if (!submitted.ok)
30631
- return fail(submitted.error);
30632
- const urlBeforeSubmit = page.url();
30633
- const confirmed = await waitUntil(async () => {
30634
- if (page.url().includes(PUBLISHED_MARKER))
30635
- return true;
30636
- return trySelectors(page, SELECTORS.publishConfirm).then((r) => r.found).catch(() => false);
30637
- }, this.stepTimeoutMs);
30638
- if (!confirmed) {
30639
- return fail(`publish submission: state unknown \u2014 the '${submitted.label}' click at ${submitted.x},${submitted.y} was sent from ${urlBeforeSubmit} but no success state was observed (now ${page.url()}). Check the ${CHANNEL} creator note manager before retrying: a retry could duplicate the post.`);
30640
- }
30641
- const url2 = page.url();
30642
- return ResultSchema.parse({
30643
- ok: true,
30644
- target: TARGET,
30645
- ...url2 !== PUBLISH_URL ? { url: url2 } : {},
30646
- message: `${TARGET} post published`,
30647
- metadata: {
30648
- ...base2,
30649
- publishControl: submitted.label,
30650
- publishClick: `${submitted.x},${submitted.y}`,
30651
- publishSignal: url2.includes(PUBLISHED_MARKER) ? `url:${PUBLISHED_MARKER}` : "text"
30652
- }
31177
+ return fail2(`login timeout after ${this.loginTimeoutMs}ms: no authenticated ${CHANNEL} creator console in ${profileDir}`);
31178
+ }
31179
+ const draftBaseline = publishStatus === "draft" ? await readDraftBaseline(page) : { count: null, stamps: [] };
31180
+ return await runMode(input, {
31181
+ page,
31182
+ content: payload.content,
31183
+ publishStatus,
31184
+ profileDir,
31185
+ driver: this.driver,
31186
+ stepTimeoutMs: this.stepTimeoutMs,
31187
+ submitSettleMs: this.submitSettleMs,
31188
+ submitAttempts: this.submitAttempts,
31189
+ draftBaseline
30653
31190
  });
30654
31191
  } catch (error51) {
30655
- return fail(error51 instanceof Error ? error51.message : String(error51));
31192
+ return fail2(error51 instanceof Error ? error51.message : String(error51));
30656
31193
  } finally {
30657
31194
  await close();
30658
31195
  }
@@ -30682,7 +31219,7 @@ async function processPublishIO(inputPath, outputPath, transport = getPublishTra
30682
31219
  }
30683
31220
  } catch (error51) {
30684
31221
  const message = `Invalid Content input: ${error51 instanceof Error ? error51.message : String(error51)}`;
30685
- await writePublishResult(outputPath, fail(message));
31222
+ await writePublishResult(outputPath, fail2(message));
30686
31223
  throw new Error(message);
30687
31224
  }
30688
31225
  const result = ResultSchema.parse(await transport.publish({ content, options }));
@@ -30723,10 +31260,16 @@ export {
30723
31260
  getPublishTransport,
30724
31261
  defaultOpenPage,
30725
31262
  defaultLauncher2 as defaultLauncher,
31263
+ creatorTabSelector,
30726
31264
  createShadowProbe,
31265
+ clickCreatorTab,
30727
31266
  checkXhsProfileSession,
30728
31267
  XhsBrowserTransport,
31268
+ XHS_OPERATIONS,
30729
31269
  PUBLISH_URL,
31270
+ MODE_LIMITS,
30730
31271
  LOGIN_TIMEOUT_MS,
31272
+ CREATOR_TAB_COUNT,
31273
+ CREATOR_TAB_BAR,
30731
31274
  CHANNEL
30732
31275
  };