@gobing-ai/knowledge-kit 0.0.36 → 0.1.0

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 (159) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/dist/board/collect/index.css +654 -16
  3. package/dist/board/collect/index.js +191 -64
  4. package/dist/board/daily-voice/index.css +679 -12
  5. package/dist/board/daily-voice/index.js +185 -66
  6. package/dist/board/itc/index.css +679 -13
  7. package/dist/board/itc/index.js +188 -66
  8. package/dist/index.js +2878 -152
  9. package/package.json +3 -2
  10. package/plugins/generations/content-gen/dist/index.js +158 -12
  11. package/plugins/generations/core-facts-gen/dist/index.js +158 -12
  12. package/plugins/generations/daily-article-gen/dist/index.js +158 -12
  13. package/plugins/generations/dailynews-gen/dist/index.js +180 -12
  14. package/plugins/generations/dailynews-gen/src/script-plan.ts +32 -0
  15. package/plugins/generations/episode-plan-gen/dist/index.js +158 -12
  16. package/plugins/generations/gemini-tts/README.md +4 -3
  17. package/plugins/generations/gemini-tts/dist/index.js +158 -12
  18. package/plugins/generations/image-gen/dist/index.js +158 -12
  19. package/plugins/generations/news-report-gen/dist/index.js +158 -12
  20. package/plugins/generations/omni-voice-gen/README.md +15 -4
  21. package/plugins/generations/omni-voice-gen/profiles.json +1 -2
  22. package/plugins/generations/voice-gen/dist/index.js +158 -12
  23. package/plugins/generations/voxcpm-tts/README.md +21 -1
  24. package/plugins/generations/voxcpm-tts/dist/index.js +173 -13
  25. package/plugins/generations/voxcpm-tts/scripts/audit-speaker-consistency.py +109 -0
  26. package/plugins/generations/voxcpm-tts/src/voxcpm-client.ts +28 -1
  27. package/plugins/ingestions/aihot-ingest/dist/index.js +158 -12
  28. package/plugins/ingestions/horizon-ingest/dist/index.js +158 -12
  29. package/plugins/ingestions/last30days-ingest/dist/index.js +158 -12
  30. package/plugins/ingestions/web-search/dist/index.js +158 -12
  31. package/plugins/kk/commands/workflow-run.md +56 -18
  32. package/plugins/kk/decisions/kk-itc.yaml +46 -0
  33. package/plugins/kk/decisions/kk-seo-keyword.yaml +57 -0
  34. package/plugins/kk/decisions/kk-seo-page.yaml +230 -0
  35. package/plugins/kk/decisions/kk-seo-pair.yaml +21 -0
  36. package/plugins/kk/decisions/kk-seo-site.yaml +90 -0
  37. package/plugins/kk/plugin.json +1 -1
  38. package/plugins/kk/publish-presets/chinese-article.json +14 -0
  39. package/plugins/kk/publish-presets/technical-article.json +14 -0
  40. package/plugins/kk/references/smart-seo-attribution.md +85 -0
  41. package/plugins/kk/scripts/daily-stages.ts +42 -2
  42. package/plugins/kk/scripts/daily-voice-style.zh.md +1 -1
  43. package/plugins/kk/scripts/itc-stages.ts +25 -118
  44. package/plugins/kk/scripts/kk-workflow-stages.ts +38 -1
  45. package/plugins/kk/scripts/seo-decisions.ts +477 -0
  46. package/plugins/kk/scripts/seo-enrich.ts +728 -0
  47. package/plugins/kk/scripts/seo-facts.ts +610 -0
  48. package/plugins/kk/scripts/seo-fetch.ts +766 -0
  49. package/plugins/kk/scripts/seo-report.ts +460 -0
  50. package/plugins/kk/scripts/seo-rules.ts +1882 -0
  51. package/plugins/kk/scripts/seo-stages.ts +1044 -0
  52. package/plugins/kk/skills/article-adapt/SKILL.md +2 -2
  53. package/plugins/kk/skills/smart-seo/SKILL.md +183 -0
  54. package/plugins/kk/skills/smart-seo/references/method.md +126 -0
  55. package/plugins/kk/skills/smart-seo/references/narrative.md +125 -0
  56. package/plugins/kk/skills/smart-seo/references/operations.md +202 -0
  57. package/plugins/kk/skills/smart-seo/references/seo-geo-sources.md +90 -0
  58. package/plugins/kk/workflows/feature-verification.yaml +28 -24
  59. package/plugins/kk/workflows/kk-daily-ai-voice2.yaml +86 -18
  60. package/plugins/kk/workflows/kk-itc-author.yaml +406 -0
  61. package/plugins/kk/workflows/kk-itc.yaml +208 -682
  62. package/plugins/kk/workflows/kk-smart-seo.yaml +251 -0
  63. package/plugins/publishings/bilibili-pub/dist/index.js +21782 -0
  64. package/plugins/publishings/bilibili-pub/dist/profile-check.js +20410 -0
  65. package/plugins/publishings/bilibili-pub/package.json +20 -0
  66. package/plugins/publishings/bilibili-pub/plugin.json +12 -0
  67. package/plugins/publishings/bilibili-pub/src/flow.ts +1296 -0
  68. package/plugins/publishings/bilibili-pub/src/index.ts +139 -0
  69. package/plugins/publishings/bilibili-pub/src/profile-check.ts +19 -0
  70. package/plugins/publishings/bilibili-pub/src/selectors.ts +222 -0
  71. package/plugins/publishings/bilibili-pub/tsconfig.json +4 -0
  72. package/plugins/publishings/cnblogs-pub/dist/index.js +21383 -0
  73. package/plugins/publishings/cnblogs-pub/dist/profile-check.js +20412 -0
  74. package/plugins/publishings/cnblogs-pub/package.json +19 -0
  75. package/plugins/publishings/cnblogs-pub/plugin.json +12 -0
  76. package/plugins/publishings/cnblogs-pub/src/flow.ts +708 -0
  77. package/plugins/publishings/cnblogs-pub/src/index.ts +172 -0
  78. package/plugins/publishings/cnblogs-pub/src/profile-check.ts +24 -0
  79. package/plugins/publishings/cnblogs-pub/src/selectors.ts +104 -0
  80. package/plugins/publishings/cnblogs-pub/tsconfig.json +4 -0
  81. package/plugins/publishings/csdn-pub/dist/index.js +21552 -0
  82. package/plugins/publishings/csdn-pub/dist/profile-check.js +20963 -0
  83. package/plugins/publishings/csdn-pub/package.json +19 -0
  84. package/plugins/publishings/csdn-pub/plugin.json +12 -0
  85. package/plugins/publishings/csdn-pub/src/index.ts +1178 -0
  86. package/plugins/publishings/csdn-pub/src/profile-check.ts +23 -0
  87. package/plugins/publishings/csdn-pub/tsconfig.json +4 -0
  88. package/plugins/publishings/emdash-pub/dist/index.js +158 -12
  89. package/plugins/publishings/emdash-pub/plugin.json +22 -1
  90. package/plugins/publishings/infoq-pub/dist/index.js +160 -12
  91. package/plugins/publishings/infoq-pub/dist/profile-check.js +160 -12
  92. package/plugins/publishings/infoq-pub/plugin.json +21 -0
  93. package/plugins/publishings/jike-pub/dist/index.js +21379 -0
  94. package/plugins/publishings/jike-pub/dist/profile-check.js +20951 -0
  95. package/plugins/publishings/jike-pub/package.json +19 -0
  96. package/plugins/publishings/jike-pub/plugin.json +12 -0
  97. package/plugins/publishings/jike-pub/src/index.ts +800 -0
  98. package/plugins/publishings/jike-pub/src/profile-check.ts +21 -0
  99. package/plugins/publishings/jike-pub/tsconfig.json +4 -0
  100. package/plugins/publishings/juejin-pub/dist/index.js +160 -12
  101. package/plugins/publishings/juejin-pub/dist/profile-check.js +160 -12
  102. package/plugins/publishings/juejin-pub/plugin.json +21 -0
  103. package/plugins/publishings/medium-pub/dist/index.js +158 -12
  104. package/plugins/publishings/medium-pub/plugin.json +22 -1
  105. package/plugins/publishings/podcast-pub/dist/index.js +158 -12
  106. package/plugins/publishings/qiita-pub/dist/index.js +158 -12
  107. package/plugins/publishings/qiita-pub/plugin.json +24 -1
  108. package/plugins/publishings/shipinhao-pub/dist/index.js +21715 -0
  109. package/plugins/publishings/shipinhao-pub/dist/profile-check.js +20431 -0
  110. package/plugins/publishings/shipinhao-pub/package.json +19 -0
  111. package/plugins/publishings/shipinhao-pub/plugin.json +12 -0
  112. package/plugins/publishings/shipinhao-pub/src/flow.ts +1323 -0
  113. package/plugins/publishings/shipinhao-pub/src/index.ts +173 -0
  114. package/plugins/publishings/shipinhao-pub/src/profile-check.ts +25 -0
  115. package/plugins/publishings/shipinhao-pub/src/selectors.ts +241 -0
  116. package/plugins/publishings/shipinhao-pub/tsconfig.json +4 -0
  117. package/plugins/publishings/sspai-pub/dist/index.js +21371 -0
  118. package/plugins/publishings/sspai-pub/dist/profile-check.js +20414 -0
  119. package/plugins/publishings/sspai-pub/package.json +19 -0
  120. package/plugins/publishings/sspai-pub/plugin.json +12 -0
  121. package/plugins/publishings/sspai-pub/src/flow.ts +624 -0
  122. package/plugins/publishings/sspai-pub/src/index.ts +167 -0
  123. package/plugins/publishings/sspai-pub/src/profile-check.ts +25 -0
  124. package/plugins/publishings/sspai-pub/src/selectors.ts +122 -0
  125. package/plugins/publishings/sspai-pub/tsconfig.json +4 -0
  126. package/plugins/publishings/substack-pub/dist/index.js +160 -12
  127. package/plugins/publishings/substack-pub/dist/profile-check.js +160 -12
  128. package/plugins/publishings/substack-pub/plugin.json +21 -0
  129. package/plugins/publishings/surfdash-pub/dist/index.js +158 -12
  130. package/plugins/publishings/surfdash-pub/plugin.json +22 -1
  131. package/plugins/publishings/threads-pub/dist/index.js +158 -12
  132. package/plugins/publishings/wechatmp-pub/dist/index.js +1669 -502
  133. package/plugins/publishings/wechatmp-pub/dist/profile-check.js +204 -21
  134. package/plugins/publishings/wechatmp-pub/plugin.json +32 -0
  135. package/plugins/publishings/wechatmp-pub/src/cgi-draft.ts +540 -0
  136. package/plugins/publishings/wechatmp-pub/src/cgi-media.ts +380 -0
  137. package/plugins/publishings/wechatmp-pub/src/cgi.ts +485 -0
  138. package/plugins/publishings/wechatmp-pub/src/flow.ts +104 -0
  139. package/plugins/publishings/weibo-pub/dist/index.js +160 -12
  140. package/plugins/publishings/weibo-pub/dist/profile-check.js +160 -12
  141. package/plugins/publishings/weibo-pub/plugin.json +35 -0
  142. package/plugins/publishings/x-pub/dist/index.js +160 -12
  143. package/plugins/publishings/x-pub/dist/profile-check.js +160 -12
  144. package/plugins/publishings/x-pub/plugin.json +40 -0
  145. package/plugins/publishings/xhs-pub/dist/index.js +160 -12
  146. package/plugins/publishings/xhs-pub/dist/profile-check.js +160 -12
  147. package/plugins/publishings/xhs-pub/plugin.json +37 -0
  148. package/plugins/publishings/xiaoyuzhou-pub/dist/index.js +21491 -0
  149. package/plugins/publishings/xiaoyuzhou-pub/dist/profile-check.js +20417 -0
  150. package/plugins/publishings/xiaoyuzhou-pub/package.json +19 -0
  151. package/plugins/publishings/xiaoyuzhou-pub/plugin.json +12 -0
  152. package/plugins/publishings/xiaoyuzhou-pub/src/flow.ts +826 -0
  153. package/plugins/publishings/xiaoyuzhou-pub/src/index.ts +174 -0
  154. package/plugins/publishings/xiaoyuzhou-pub/src/profile-check.ts +26 -0
  155. package/plugins/publishings/xiaoyuzhou-pub/src/selectors.ts +125 -0
  156. package/plugins/publishings/xiaoyuzhou-pub/tsconfig.json +4 -0
  157. package/plugins/publishings/zenn-pub/dist/index.js +158 -12
  158. package/plugins/publishings/zenn-pub/plugin.json +22 -1
  159. package/plugins/generations/omni-voice-gen/voices/robin-news.wav +0 -0
@@ -19831,6 +19831,83 @@ var init_runtime_node_bun = __esm(() => {
19831
19831
  // ../../plugins/publishings/wechatmp-pub/src/index.ts
19832
19832
  import { parseArgs } from "util";
19833
19833
 
19834
+ // ../../packages/kk-core/src/content-limits.ts
19835
+ var PLATFORM_CONTENT_LIMITS = Object.freeze({
19836
+ schemaVersion: 5,
19837
+ tagLimits: Object.freeze({
19838
+ default: 7,
19839
+ imageText: Object.freeze({
19840
+ instagram: 5,
19841
+ douyin: 5,
19842
+ kuaishou: 4,
19843
+ weixin: 1,
19844
+ xiaohongshu: 10
19845
+ }),
19846
+ shortVideo: Object.freeze({
19847
+ douyin: 5,
19848
+ kuaishou: 4,
19849
+ xiaohongshu: 10,
19850
+ bilibili: 10,
19851
+ zhihu: 5,
19852
+ tiktok: 20,
19853
+ youtube: 60
19854
+ })
19855
+ }),
19856
+ titleCapabilities: Object.freeze({
19857
+ dynamic: Object.freeze({
19858
+ bilibili: Object.freeze({ max: 20, unit: "utf16_code_units" }),
19859
+ zhihu: Object.freeze({ max: 50, unit: "utf16_code_units" }),
19860
+ douban: Object.freeze({ max: 70, unit: "utf16_code_units" }),
19861
+ x: null,
19862
+ threads: null,
19863
+ weibo: null,
19864
+ jike: null,
19865
+ zsxq: null,
19866
+ toutiao: null,
19867
+ juejin: null
19868
+ }),
19869
+ imageText: Object.freeze({
19870
+ xiaohongshu: Object.freeze({ max: 20, unit: "xiaohongshu_title_weighted" }),
19871
+ weixin: Object.freeze({ max: 20, unit: "utf16_code_units" }),
19872
+ douyin: Object.freeze({ max: 20, unit: "utf16_code_units" }),
19873
+ shipinhao: Object.freeze({ max: 22, unit: "utf16_code_units" }),
19874
+ reddit: Object.freeze({ max: 300, unit: "utf16_code_units" })
19875
+ }),
19876
+ article: Object.freeze({
19877
+ juejin: Object.freeze({ max: 100, unit: "utf16_code_units" }),
19878
+ csdn: Object.freeze({ max: 100, unit: "utf16_code_units" }),
19879
+ cnblogs: Object.freeze({ max: 128, unit: "utf16_code_units" }),
19880
+ weibo: Object.freeze({ max: 30, unit: "utf16_code_units" }),
19881
+ sspai: Object.freeze({ max: 80, unit: "utf16_code_units" })
19882
+ }),
19883
+ shortVideo: Object.freeze({
19884
+ shipinhao: Object.freeze({ max: 16, unit: "utf16_code_units" }),
19885
+ douyin: Object.freeze({ max: 30, unit: "utf16_code_units" }),
19886
+ bilibili: Object.freeze({ max: 80, unit: "utf16_code_units" })
19887
+ })
19888
+ }),
19889
+ bodyCapabilities: Object.freeze({
19890
+ dynamic: Object.freeze({
19891
+ threads: Object.freeze({ max: 500, unit: "unicode_code_points", behavior: "split_thread" }),
19892
+ x: Object.freeze({ max: 280, unit: "weighted", behavior: "split_thread" }),
19893
+ weibo: Object.freeze({ max: 2000, unit: "unicode_code_points" }),
19894
+ jike: Object.freeze({ max: 1000, unit: "unicode_code_points" })
19895
+ }),
19896
+ imageText: Object.freeze({
19897
+ xiaohongshu: Object.freeze({ max: 1000, unit: "unicode_code_points" }),
19898
+ douyin: Object.freeze({ max: 1000, unit: "unicode_code_points" })
19899
+ }),
19900
+ article: Object.freeze({
19901
+ wechatmp: Object.freeze({ max: 50000, unit: "unicode_code_points" }),
19902
+ juejin: Object.freeze({ max: 50000, unit: "unicode_code_points" })
19903
+ }),
19904
+ shortVideo: Object.freeze({
19905
+ douyin: Object.freeze({ max: 1000, unit: "unicode_code_points" }),
19906
+ kuaishou: Object.freeze({ max: 1000, unit: "unicode_code_points" }),
19907
+ shipinhao: Object.freeze({ max: 1000, unit: "unicode_code_points" })
19908
+ })
19909
+ })
19910
+ });
19834
19911
  // ../../packages/kk-core/src/kinds.ts
19835
19912
  init_zod();
19836
19913
 
@@ -19921,6 +19998,67 @@ var BrowserSessionDescriptorSchema = object({
19921
19998
  }
19922
19999
  }).optional()
19923
20000
  }).strict();
20001
+ var ContentCountUnitSchema = _enum([
20002
+ "utf16_code_units",
20003
+ "unicode_code_points",
20004
+ "weighted",
20005
+ "xiaohongshu_title_weighted"
20006
+ ]);
20007
+ var LimitEntrySchema = object({
20008
+ max: number2().int().positive(),
20009
+ unit: ContentCountUnitSchema
20010
+ }).strict();
20011
+ var PublishingProfileLimitsSchema = object({
20012
+ title: LimitEntrySchema.optional(),
20013
+ body: LimitEntrySchema.optional()
20014
+ }).strict();
20015
+ var PublishingProfileRequiresSchema = object({
20016
+ cover: boolean2().optional(),
20017
+ orderedImages: boolean2().optional()
20018
+ }).strict();
20019
+ var PublishingProfileModeBindingSchema = object({
20020
+ metadata: record(string2(), unknown())
20021
+ }).strict();
20022
+ var PublishingProfileModesSchema = object({
20023
+ draft: PublishingProfileModeBindingSchema.optional(),
20024
+ live: PublishingProfileModeBindingSchema.optional()
20025
+ }).strict().superRefine((modes, ctx) => {
20026
+ if (modes.draft === undefined && modes.live === undefined) {
20027
+ ctx.addIssue({
20028
+ code: "custom",
20029
+ message: 'modes must define at least one of "draft" or "live"'
20030
+ });
20031
+ }
20032
+ });
20033
+ var PublishingProfileSchema = object({
20034
+ format: string2().min(1),
20035
+ locale: string2().regex(/^(source|[a-z]{2,3}(-[A-Za-z0-9]+)*)$/, {
20036
+ message: 'locale must be "source" or a valid language tag'
20037
+ }),
20038
+ limits: PublishingProfileLimitsSchema.optional(),
20039
+ requires: PublishingProfileRequiresSchema.optional(),
20040
+ modes: PublishingProfileModesSchema
20041
+ }).strict();
20042
+ var PublishingCapabilitiesSchema = object({
20043
+ defaultProfile: string2().min(1),
20044
+ profiles: record(string2().regex(/^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/), PublishingProfileSchema)
20045
+ }).strict().superRefine((data, ctx) => {
20046
+ const keys = Object.keys(data.profiles);
20047
+ if (keys.length === 0) {
20048
+ ctx.addIssue({
20049
+ code: "custom",
20050
+ path: ["profiles"],
20051
+ message: "profiles record must be nonempty"
20052
+ });
20053
+ }
20054
+ if (!(data.defaultProfile in data.profiles)) {
20055
+ ctx.addIssue({
20056
+ code: "custom",
20057
+ path: ["defaultProfile"],
20058
+ message: `defaultProfile "${data.defaultProfile}" must reference a declared profile`
20059
+ });
20060
+ }
20061
+ });
19924
20062
  var PluginManifestSchemaBase = looseObject({
19925
20063
  name: string2().min(1),
19926
20064
  kind: PluginKindSchema,
@@ -19929,23 +20067,31 @@ var PluginManifestSchemaBase = looseObject({
19929
20067
  version: string2().optional(),
19930
20068
  description: string2().optional(),
19931
20069
  operations: record(string2(), PluginOperationSchema).optional(),
19932
- browserSession: BrowserSessionDescriptorSchema.optional()
20070
+ browserSession: BrowserSessionDescriptorSchema.optional(),
20071
+ publishing: PublishingCapabilitiesSchema.optional()
19933
20072
  });
19934
20073
  var PluginManifestSchema = PluginManifestSchemaBase.superRefine((manifest, ctx) => {
19935
- if (manifest.browserSession === undefined)
19936
- return;
19937
- if (manifest.kind !== "publish") {
19938
- ctx.addIssue({
19939
- code: "custom",
19940
- path: ["browserSession"],
19941
- message: `browserSession requires kind "publish", not "${manifest.kind}"`
19942
- });
20074
+ if (manifest.browserSession !== undefined) {
20075
+ if (manifest.kind !== "publish") {
20076
+ ctx.addIssue({
20077
+ code: "custom",
20078
+ path: ["browserSession"],
20079
+ message: `browserSession requires kind "publish", not "${manifest.kind}"`
20080
+ });
20081
+ }
20082
+ if (manifest.check === undefined) {
20083
+ ctx.addIssue({
20084
+ code: "custom",
20085
+ path: ["browserSession"],
20086
+ message: "browserSession requires a check probe entry that owns the session routines"
20087
+ });
20088
+ }
19943
20089
  }
19944
- if (manifest.check === undefined) {
20090
+ if (manifest.publishing !== undefined && manifest.kind !== "publish") {
19945
20091
  ctx.addIssue({
19946
20092
  code: "custom",
19947
- path: ["browserSession"],
19948
- message: "browserSession requires a check probe entry that owns the session routines"
20093
+ path: ["publishing"],
20094
+ message: `publishing requires kind "publish", not "${manifest.kind}"`
19949
20095
  });
19950
20096
  }
19951
20097
  });
@@ -20484,6 +20630,8 @@ async function ensureProfileDir2(profileDir, headless) {
20484
20630
  throw new BrowserProfileMissingError(profileDir);
20485
20631
  await mkdir2(profileDir, { recursive: true, mode: 448 });
20486
20632
  }
20633
+ // ../../packages/publish-harness/src/browser-anti-detection.ts
20634
+ var MAX_QUEUED_BYTES = 8 * 1024 * 1024;
20487
20635
  // ../../packages/publish-harness/src/browser.ts
20488
20636
  var DEFAULT_STEP_TIMEOUT_MS = 30000;
20489
20637
  var DEFAULT_POLL_MS = 500;
@@ -20687,380 +20835,344 @@ function editorTextScript(selector) {
20687
20835
  // ../../plugins/publishings/wechatmp-pub/src/index.ts
20688
20836
  init_dist();
20689
20837
 
20690
- // ../../plugins/publishings/wechatmp-pub/src/page.ts
20691
- var POLL_MS = 500;
20692
- var STEP_TIMEOUT_MS = 30000;
20693
- async function modalState(page) {
20694
- try {
20695
- const raw = await page.evaluate(`[...document.querySelectorAll('.weui-desktop-dialog__wrp, .weui-desktop-dialog')].some((d) => d.offsetWidth)`);
20696
- return raw === true ? "open" : raw === false ? "closed" : "unknown";
20697
- } catch {
20698
- return "unknown";
20838
+ // ../../plugins/publishings/wechatmp-pub/src/cgi.ts
20839
+ var MP_ORIGIN = "https://mp.weixin.qq.com";
20840
+ var FILETRANSFER_URL = `${MP_ORIGIN}/cgi-bin/filetransfer`;
20841
+ var CROPIMAGE_URL = `${MP_ORIGIN}/cgi-bin/cropimage`;
20842
+ var MAX_RESPONSE_BYTES = 2 * 1024 * 1024;
20843
+ var REQUEST_TIMEOUT_MS = 30000;
20844
+ var MAX_IMAGE_BYTES = 10 * 1024 * 1024;
20845
+ var WIDE_COVER_ASPECT = 2.35;
20846
+ var SQUARE_COVER_ASPECT = 1;
20847
+ var EXTENSION_MIME = {
20848
+ ".png": "image/png",
20849
+ ".jpg": "image/jpeg",
20850
+ ".jpeg": "image/jpeg",
20851
+ ".webp": "image/webp"
20852
+ };
20853
+
20854
+ class WechatCgiError extends Error {
20855
+ reason;
20856
+ constructor(reason, message) {
20857
+ super(message);
20858
+ this.reason = reason;
20859
+ this.name = "WechatCgiError";
20699
20860
  }
20700
20861
  }
20701
- async function tryFindAttachedSelector(page, selectors, timeoutMs = STEP_TIMEOUT_MS) {
20702
- let found;
20703
- await waitUntil(async () => {
20704
- for (const selector of selectors) {
20705
- const attached = await page.locator(selector).count().catch(() => 0);
20706
- if (attached > 0) {
20707
- found = selector;
20708
- return true;
20709
- }
20710
- }
20862
+ function isAllowedCdnUrl(candidate) {
20863
+ try {
20864
+ const url = new URL(candidate);
20865
+ return url.protocol === "https:" && url.host === "mmbiz.qpic.cn";
20866
+ } catch {
20711
20867
  return false;
20712
- }, timeoutMs);
20713
- return found;
20868
+ }
20714
20869
  }
20715
- async function tryFindSelector(page, selectors, timeoutMs = STEP_TIMEOUT_MS) {
20716
- let found;
20717
- await waitUntil(async () => {
20718
- const result = await trySelectors(page, selectors).catch(() => ({ found: false, selector: null }));
20719
- if (result.found && result.selector) {
20720
- found = result.selector;
20721
- return true;
20722
- }
20723
- return false;
20724
- }, timeoutMs);
20725
- return found;
20870
+ function centerCropRect(width, height, aspect) {
20871
+ if (!Number.isFinite(width) || !Number.isFinite(height) || width < 1 || height < 1) {
20872
+ throw new WechatCgiError("invalid-dimensions", `image dimensions must be positive numbers, got ${width}x${height}`);
20873
+ }
20874
+ if (!Number.isFinite(aspect) || aspect <= 0) {
20875
+ throw new WechatCgiError("invalid-dimensions", `crop aspect must be positive, got ${aspect}`);
20876
+ }
20877
+ let cropWidth = width;
20878
+ let cropHeight = height;
20879
+ if (width / height > aspect) {
20880
+ cropWidth = height * aspect;
20881
+ } else {
20882
+ cropHeight = width / aspect;
20883
+ }
20884
+ const x1 = Math.floor((width - cropWidth) / 2);
20885
+ const y1 = Math.floor((height - cropHeight) / 2);
20886
+ return {
20887
+ x1,
20888
+ y1,
20889
+ x2: Math.min(width, x1 + Math.round(cropWidth)),
20890
+ y2: Math.min(height, y1 + Math.round(cropHeight))
20891
+ };
20726
20892
  }
20727
- async function findSelector(page, selectors, what, timeoutMs = STEP_TIMEOUT_MS) {
20728
- const found = await tryFindSelector(page, selectors, timeoutMs);
20729
- if (found === undefined) {
20730
- throw new Error(`${what}: none of [${selectors.join(" | ")}] visible within ${timeoutMs}ms`);
20893
+ function extensionMime(name) {
20894
+ const lower = name.toLowerCase();
20895
+ const dot = lower.lastIndexOf(".");
20896
+ const mime = dot === -1 ? undefined : EXTENSION_MIME[lower.slice(dot)];
20897
+ if (mime === undefined) {
20898
+ throw new WechatCgiError("unsupported-extension", `accepted image extensions are PNG/JPEG/WebP, got '${name}'`);
20731
20899
  }
20732
- return found;
20900
+ return mime;
20733
20901
  }
20734
- async function findAttachedSelector(page, selectors, what, timeoutMs = STEP_TIMEOUT_MS) {
20735
- const found = await tryFindAttachedSelector(page, selectors, timeoutMs);
20736
- if (found === undefined) {
20737
- throw new Error(`${what}: none of [${selectors.join(" | ")}] attached within ${timeoutMs}ms`);
20902
+ function validateImageBytes(bytes, name) {
20903
+ if (bytes.byteLength === 0) {
20904
+ throw new WechatCgiError("empty-image", `image '${name}' is empty`);
20738
20905
  }
20739
- return found;
20906
+ if (bytes.byteLength > MAX_IMAGE_BYTES) {
20907
+ throw new WechatCgiError("image-too-large", `image '${name}' exceeds the ${MAX_IMAGE_BYTES}-byte bound`);
20908
+ }
20909
+ return { bytes, name, mime: extensionMime(name) };
20740
20910
  }
20741
- async function followNewPage(pages, before, timeoutMs) {
20742
- const opened = await waitUntil(async () => pages().length > before, timeoutMs);
20743
- if (!opened)
20744
- return;
20745
- const all = pages();
20746
- return all[all.length - 1];
20911
+ async function readLocalImageBytes(path) {
20912
+ const name = path.replaceAll("\\", "/").split("/").pop() ?? path;
20913
+ extensionMime(name);
20914
+ const file = Bun.file(path);
20915
+ let stat;
20916
+ try {
20917
+ stat = await file.stat();
20918
+ } catch {
20919
+ throw new WechatCgiError("unreadable-file", `image is not a readable file: ${path}`);
20920
+ }
20921
+ if (!stat.isFile() || stat.size === 0) {
20922
+ throw new WechatCgiError("unreadable-file", `image is not a nonempty regular file: ${path}`);
20923
+ }
20924
+ if (stat.size > MAX_IMAGE_BYTES) {
20925
+ throw new WechatCgiError("image-too-large", `image '${path}' exceeds the ${MAX_IMAGE_BYTES}-byte bound`);
20926
+ }
20927
+ return validateImageBytes(new Uint8Array(await file.arrayBuffer()), name);
20747
20928
  }
20748
- async function clickSelector(page, selector, timeoutMs = STEP_TIMEOUT_MS) {
20749
- await page.locator(selector).first().click({ timeout: timeoutMs });
20929
+ function cgiFetchExpression(spec) {
20930
+ return `(async () => {
20931
+ const spec = ${JSON.stringify(spec)};
20932
+ const controller = new AbortController();
20933
+ const timer = setTimeout(() => controller.abort(), spec.timeoutMs);
20934
+ try {
20935
+ const form = new FormData();
20936
+ for (const key of Object.keys(spec.fields)) form.append(key, spec.fields[key]);
20937
+ if (spec.file) {
20938
+ const binary = atob(spec.file.base64);
20939
+ const bytes = new Uint8Array(binary.length);
20940
+ for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
20941
+ form.append(spec.file.field, new Blob([bytes], { type: spec.file.mime }), spec.file.filename);
20942
+ }
20943
+ const response = await fetch(spec.url, {
20944
+ method: 'POST',
20945
+ body: form,
20946
+ credentials: 'include',
20947
+ redirect: 'error',
20948
+ signal: controller.signal,
20949
+ });
20950
+ if (!response.ok) return JSON.stringify({ kind: 'http-status', status: response.status });
20951
+ if (!response.body) return JSON.stringify({ kind: 'error', detail: 'response has no body stream' });
20952
+ const reader = response.body.getReader();
20953
+ let total = 0;
20954
+ const chunks = [];
20955
+ for (;;) {
20956
+ const { done, value } = await reader.read();
20957
+ if (done) break;
20958
+ total += value.byteLength;
20959
+ if (total > spec.maxBytes) {
20960
+ controller.abort();
20961
+ return JSON.stringify({ kind: 'too-large' });
20962
+ }
20963
+ chunks.push(value);
20964
+ }
20965
+ const text = await new Blob(chunks).text();
20966
+ return JSON.stringify({ kind: 'ok', status: response.status, text });
20967
+ } catch (error) {
20968
+ return JSON.stringify({ kind: 'error', detail: String(error) });
20969
+ } finally {
20970
+ clearTimeout(timer);
20971
+ }
20972
+ })()`;
20750
20973
  }
20751
- async function fillSelector(page, selector, value) {
20752
- await page.locator(selector).first().fill(value);
20974
+ function stripQuery(detail) {
20975
+ return detail.replace(/\?[^'"\\\s]*/g, "?\u2026");
20753
20976
  }
20754
- function wrapPlaywrightPage(raw) {
20755
- const pw = raw;
20756
- return {
20757
- goto: (url, options) => pw.goto(url, { waitUntil: "domcontentloaded", ...options }).then(() => {
20758
- return;
20759
- }),
20760
- locator: (selector) => ({
20761
- count: () => pw.locator(selector).count(),
20762
- first: () => {
20763
- const loc = pw.locator(selector).first();
20764
- return {
20765
- count: () => loc.count(),
20766
- isVisible: (options) => loc.isVisible(options),
20767
- fill: (text, options) => loc.fill(text, options),
20768
- click: (options) => loc.click(options),
20769
- type: (text, options) => loc.pressSequentially(text, options),
20770
- waitFor: (options) => loc.waitFor(options),
20771
- textContent: async () => await loc.textContent() ?? "",
20772
- inputValue: () => loc.inputValue()
20773
- };
20774
- }
20775
- }),
20776
- setInputFiles: (selector, paths) => pw.setInputFiles(selector, paths),
20777
- url: () => pw.url(),
20778
- evaluate: (expression) => pw.evaluate(expression),
20779
- waitForTimeout: (ms) => pw.waitForTimeout(ms),
20780
- press: (key) => pw.keyboard.press(key),
20781
- frames: () => {
20782
- const source = raw;
20783
- return (source.frames?.() ?? []).map((frame) => ({
20784
- url: () => frame.url(),
20785
- locator: (selector) => ({ count: () => frame.locator(selector).count() })
20786
- }));
20977
+ async function postCgiForm(page, spec) {
20978
+ let verdict;
20979
+ const raw = await page.evaluate(cgiFetchExpression(spec));
20980
+ if (typeof raw === "string") {
20981
+ try {
20982
+ verdict = JSON.parse(raw);
20983
+ } catch {
20984
+ verdict = undefined;
20787
20985
  }
20788
- };
20986
+ }
20987
+ if (verdict?.kind === "http-status") {
20988
+ throw new WechatCgiError("http-status", `CGI call returned HTTP ${verdict.status}`);
20989
+ }
20990
+ if (verdict?.kind === "too-large") {
20991
+ throw new WechatCgiError("response-too-large", `CGI response exceeded the ${MAX_RESPONSE_BYTES}-byte bound`);
20992
+ }
20993
+ if (verdict?.kind !== "ok") {
20994
+ const detail = verdict?.kind === "error" ? stripQuery(verdict.detail) : "page returned no usable verdict";
20995
+ throw new WechatCgiError("network-error", `CGI call failed before a usable response: ${detail}`);
20996
+ }
20997
+ return { status: verdict.status, text: verdict.text };
20789
20998
  }
20790
- function wrapPlaywrightContext(raw, entryPage) {
20791
- const context = raw;
20792
- const cache = new WeakMap;
20793
- const wrap = (page) => {
20794
- const key = page;
20795
- const cached = cache.get(key);
20796
- if (cached !== undefined)
20797
- return cached;
20798
- const wrapped = wrapPlaywrightPage(page);
20799
- cache.set(key, wrapped);
20800
- return wrapped;
20801
- };
20802
- return {
20803
- page: wrap(entryPage),
20804
- pages: () => context.pages().map(wrap),
20805
- newPage: async () => wrap(await context.newPage()),
20806
- close: () => context.close()
20807
- };
20999
+ function isRecord(value) {
21000
+ return typeof value === "object" && value !== null;
20808
21001
  }
20809
- async function waitForVisible(page, selector, timeoutMs, opts = {}) {
20810
- const visible = await waitUntil(async () => {
20811
- const found = await trySelectors(page, [selector]).catch(() => ({ found: false, selector: null }));
20812
- return found.found;
20813
- }, timeoutMs, POLL_MS);
20814
- if (visible)
20815
- return selector;
20816
- if (opts.required === false)
21002
+ function parseJsonBody(text, what) {
21003
+ let body;
21004
+ try {
21005
+ body = JSON.parse(text);
21006
+ } catch {
21007
+ throw new WechatCgiError("bad-receipt", `${what}: response is not JSON`);
21008
+ }
21009
+ if (!isRecord(body)) {
21010
+ throw new WechatCgiError("bad-receipt", `${what}: response is not a JSON object`);
21011
+ }
21012
+ return body;
21013
+ }
21014
+ function assertBaseResp(body, what) {
21015
+ const base = body.base_resp;
21016
+ if (base === undefined)
20817
21017
  return;
20818
- throw new Error(`${selector}: not visible within ${timeoutMs}ms`);
21018
+ if (!isRecord(base)) {
21019
+ throw new WechatCgiError("bad-receipt", `${what}: malformed base_resp`);
21020
+ }
21021
+ if (base.ret !== undefined && base.ret !== 0) {
21022
+ throw new WechatCgiError("bad-receipt", `${what}: base_resp.ret=${String(base.ret)}`);
21023
+ }
21024
+ if (base.err_msg !== undefined && base.err_msg !== "ok") {
21025
+ throw new WechatCgiError("bad-receipt", `${what}: err_msg=${String(base.err_msg)}`);
21026
+ }
21027
+ }
21028
+ function parseFiletransferReceipt(text) {
21029
+ const body = parseJsonBody(text, "filetransfer");
21030
+ assertBaseResp(body, "filetransfer");
21031
+ const content = body.content;
21032
+ if (typeof content !== "string" || !/^\d+$/.test(content)) {
21033
+ throw new WechatCgiError("bad-receipt", "filetransfer: content file ID is missing or not numeric");
21034
+ }
21035
+ const cdnUrl = body.cdn_url;
21036
+ if (typeof cdnUrl !== "string" || !isAllowedCdnUrl(cdnUrl)) {
21037
+ throw new WechatCgiError("bad-receipt", "filetransfer: cdn_url is missing or outside the allowed HTTPS CDN host");
21038
+ }
21039
+ const width = body.width;
21040
+ const height = body.height;
21041
+ if (typeof width !== "number" || typeof height !== "number" || width < 1 || height < 1) {
21042
+ throw new WechatCgiError("bad-receipt", "filetransfer: receipt did not return usable image dimensions");
21043
+ }
21044
+ return { fileId: content, url: cdnUrl, width, height };
21045
+ }
21046
+ function parseCropMultiReceipt(text, crops) {
21047
+ const body = parseJsonBody(text, "crop_multi");
21048
+ assertBaseResp(body, "crop_multi");
21049
+ const entries = Array.isArray(body.result) ? body.result : [];
21050
+ const entryCount = entries.length;
21051
+ if (entryCount !== 2) {
21052
+ throw new WechatCgiError("bad-receipt", `crop_multi: expected exactly two result entries, got ${entryCount}`);
21053
+ }
21054
+ let wide;
21055
+ let square;
21056
+ entries.forEach((entry, index) => {
21057
+ const dims = index === 0 ? crops[0] : crops[1];
21058
+ if (!isRecord(entry)) {
21059
+ throw new WechatCgiError("bad-receipt", `crop_multi: result entry ${index} is not an object`);
21060
+ }
21061
+ const cdnurl = entry.cdnurl;
21062
+ const fileId = entry.file_id;
21063
+ if (typeof cdnurl !== "string" || !isAllowedCdnUrl(cdnurl)) {
21064
+ throw new WechatCgiError("bad-receipt", `crop_multi: entry ${index} cdnurl is outside the allowed HTTPS CDN host`);
21065
+ }
21066
+ if (typeof fileId !== "string" || fileId === "") {
21067
+ throw new WechatCgiError("bad-receipt", `crop_multi: entry ${index} file_id is missing`);
21068
+ }
21069
+ const image = {
21070
+ fileId,
21071
+ url: cdnurl,
21072
+ width: Math.max(1, dims.x2 - dims.x1),
21073
+ height: Math.max(1, dims.y2 - dims.y1)
21074
+ };
21075
+ if (index === 0) {
21076
+ wide = image;
21077
+ } else {
21078
+ square = image;
21079
+ }
21080
+ });
21081
+ if (wide === undefined || square === undefined) {
21082
+ throw new WechatCgiError("bad-receipt", "crop_multi: receipt did not fill both crop entries");
21083
+ }
21084
+ return [wide, square];
20819
21085
  }
20820
-
20821
- // ../../plugins/publishings/wechatmp-pub/src/enrich.ts
20822
- var DIALOG = ".weui-desktop-dialog:visible";
20823
- var CONFIRM = `${DIALOG} button:has-text("\u786E\u8BA4"), ${DIALOG} button:has-text("\u786E\u5B9A")`;
20824
- var ROW_ORIGINAL = [
20825
- ".setting-group__switch.js_original_apply.js_edit_ori",
20826
- ".setting-group__switch.js_edit_ori",
20827
- ".js_edit_ori",
20828
- "text=\u539F\u521B"
20829
- ];
20830
- var ROW_REWARD = [
20831
- ".setting-group__switch.js_reward_open",
20832
- ".js_reward_open",
20833
- ".js_reward_allow_click",
20834
- ".setting-group__switch.js_can_reward",
20835
- "text=\u8D5E\u8D4F"
20836
- ];
20837
- var ROW_COLLECTION = [".allow_click_opr.js_article_tags_label", "text=\u5408\u96C6"];
20838
- var ROW_URL = [".allow_click_opr.js_url_checkbox", "text=\u539F\u6587\u94FE\u63A5"];
20839
- var ROW_CLAIM = ["#js_claim_source_area .frm_checkbox_label", "text=\u521B\u4F5C\u6765\u6E90"];
20840
- var DIALOG_CONFIRM = CONFIRM;
20841
- var TAG_INPUT = 'input[placeholder*="\u5408\u96C6"]';
20842
- var REWARD_ACCOUNT = [
20843
- `${DIALOG} input[placeholder*="\u8D5E\u8D4F"]`,
20844
- `${DIALOG} input[placeholder*="\u8D26\u6237"]`,
20845
- `${DIALOG} input.frm_input`
20846
- ];
20847
- var URL_INPUT = [`${DIALOG} input[type="url"]`, `${DIALOG} input[placeholder*="\u94FE\u63A5"]`, `${DIALOG} input.frm_input`];
20848
- var ORIGINAL_DECLARATION = `${DIALOG} label:has(.js_original_type_radio):has-text("\u6587\u5B57\u539F\u521B")`;
20849
- var ORIGINAL_AGREEMENT = `${DIALOG} .original_agreement`;
20850
- var COLLECTION_SETTLE_MS = 1200;
20851
- var STEP_RETRY_SETTLE_MS = 1000;
20852
- var SWEEP_SETTLE_MS = 400;
20853
-
20854
- class CapabilityError extends Error {
21086
+ function buildUrl(base, params) {
21087
+ return `${base}?${new URLSearchParams(params).toString()}`;
20855
21088
  }
20856
- function norm(value) {
20857
- return typeof value === "string" ? value.replace(/\s+/g, " ").trim() : "";
21089
+ function randomRequestSalt() {
21090
+ return Math.floor(Math.random() * 1e9);
20858
21091
  }
20859
- var COMMITTED = {
20860
- original: `(() => {
20861
- const el = document.querySelector('#js_original, .js_ori_setting_checkbox');
20862
- return el ? el.checked === true : null;
20863
- })()`,
20864
- reward: `(() => {
20865
- const el = document.querySelector('#js_reward_setting_area .js_reward_setting_checkbox') ||
20866
- document.querySelector('.js_reward_allow_click_cell .js_reward_setting');
20867
- return el ? el.checked === true : null;
20868
- })()`
20869
- };
20870
- var CLAIM_COMMITTED = `(() => {
20871
- const area = document.querySelector('#js_claim_source_area');
20872
- if (!area) return null;
20873
- const label = area.querySelector('.js_claim_source_selected');
20874
- const text = (label ? (label.textContent ?? '') : '').replace(/\\s+/g, ' ').trim();
20875
- const checkbox = area.querySelector('.js_claim_source, input[type=checkbox]');
20876
- const enabled = checkbox ? checkbox.checked === true : null;
20877
- return { enabled, text };
20878
- })()`;
20879
- async function readChecked(page, expression) {
21092
+ function filetransferUrl(session) {
21093
+ return buildUrl(FILETRANSFER_URL, {
21094
+ action: "upload_material",
21095
+ f: "json",
21096
+ scene: "8",
21097
+ writetype: "doublewrite",
21098
+ groupid: "1",
21099
+ ticket_id: session.userName,
21100
+ ticket: session.ticket,
21101
+ svr_time: String(Math.floor(Date.now() / 1000)),
21102
+ token: session.token,
21103
+ lang: "zh_CN",
21104
+ seq: String(randomRequestSalt()),
21105
+ t: String(randomRequestSalt())
21106
+ });
21107
+ }
21108
+ function assertMpOrigin(page) {
21109
+ let origin;
20880
21110
  try {
20881
- const raw = await page.evaluate(expression);
20882
- return typeof raw === "boolean" ? raw : null;
21111
+ origin = new URL(page.url()).origin;
20883
21112
  } catch {
20884
- return null;
20885
- }
20886
- }
20887
- async function enrichArticle(page, meta) {
20888
- const warnings = [];
20889
- const str = (key) => typeof meta[key] === "string" ? meta[key] : "";
20890
- const on = (key) => meta[key] === true;
20891
- const sweep = async () => {
20892
- const cancel = await trySelectors(page, ['button:has-text("\u53D6\u6D88")']);
20893
- if (cancel.found) {
20894
- try {
20895
- await page.locator(cancel.selector ?? "").first().click({ timeout: 2000 });
20896
- } catch {}
20897
- }
20898
- await page.press?.("Escape");
20899
- await page.waitForTimeout?.(SWEEP_SETTLE_MS);
20900
- const state = await modalState(page);
20901
- if (state === "open") {
20902
- for (const close of [".weui-desktop-dialog__close-btn", ".weui-desktop-dialog__close"]) {
20903
- const control = await tryFindSelector(page, [close], 1500);
20904
- if (control === undefined)
20905
- continue;
20906
- try {
20907
- await clickSelector(page, control, 2000);
20908
- } catch {}
20909
- break;
20910
- }
20911
- return await modalState(page) === "closed" ? "closed" : "unknown";
20912
- }
20913
- return state === "closed" ? "closed" : "unknown";
21113
+ throw new WechatCgiError("wrong-origin", `page URL is not usable for CGI dispatch: ${page.url()}`);
21114
+ }
21115
+ if (origin !== MP_ORIGIN) {
21116
+ throw new WechatCgiError("wrong-origin", `CGI dispatch requires page origin ${MP_ORIGIN}, got ${origin}`);
21117
+ }
21118
+ }
21119
+ async function uploadCoverImage(page, session, input) {
21120
+ assertMpOrigin(page);
21121
+ const image = "path" in input ? await readLocalImageBytes(input.path) : validateImageBytes(input.bytes, input.name);
21122
+ const now = Date.now();
21123
+ const fields = {
21124
+ type: image.mime,
21125
+ id: `kk_${now}_${randomRequestSalt()}`,
21126
+ name: image.name,
21127
+ lastModifiedDate: new Date(now).toString(),
21128
+ size: String(image.bytes.byteLength)
20914
21129
  };
20915
- const openRow = async (row, what) => {
20916
- for (const selector of row) {
20917
- const found = await trySelectors(page, [selector]);
20918
- if (found.found) {
20919
- await clickSelector(page, selector, 8000);
20920
- return;
20921
- }
20922
- }
20923
- throw new Error(`no ${what} row control among [${row.join(" | ")}]`);
20924
- };
20925
- const step = async (name, fn) => {
20926
- try {
20927
- await fn();
20928
- return "ok";
20929
- } catch (error) {
20930
- if (error instanceof CapabilityError) {
20931
- warnings.push(`${name}: ${error.message}`);
20932
- return "capability";
20933
- }
20934
- }
20935
- await sweep();
20936
- await page.waitForTimeout?.(STEP_RETRY_SETTLE_MS);
20937
- try {
20938
- await fn();
20939
- return "ok";
20940
- } catch (error) {
20941
- warnings.push(`${name}: ${(error instanceof Error ? error.message : String(error)).slice(0, 120)}`);
20942
- await sweep();
20943
- return "failed";
20944
- }
20945
- };
20946
- const residual = async (name) => {
20947
- const state = await modalState(page);
20948
- if (state === "closed")
20949
- return false;
20950
- warnings.push(state === "open" ? `${name}: dialog did not close \u2014 check the draft` : `${name}: dialog state unknown after cleanup \u2014 a residual overlay may block later steps`);
20951
- return true;
21130
+ const { text } = await postCgiForm(page, {
21131
+ url: filetransferUrl(session),
21132
+ fields,
21133
+ file: {
21134
+ field: "file",
21135
+ filename: image.name,
21136
+ mime: image.mime,
21137
+ base64: Buffer.from(image.bytes).toString("base64")
21138
+ },
21139
+ maxBytes: MAX_RESPONSE_BYTES,
21140
+ timeoutMs: REQUEST_TIMEOUT_MS
21141
+ });
21142
+ return parseFiletransferReceipt(text);
21143
+ }
21144
+ async function cropCoverMulti(page, session, imageUrl, crops) {
21145
+ assertMpOrigin(page);
21146
+ const fields = {
21147
+ imgurl: imageUrl,
21148
+ size_count: "2",
21149
+ token: session.token,
21150
+ lang: "zh_CN",
21151
+ f: "json",
21152
+ ajax: "1"
20952
21153
  };
20953
- if (on("original")) {
20954
- const name = "\u539F\u521B\u58F0\u660E";
20955
- if (await readChecked(page, COMMITTED.original) !== true) {
20956
- const outcome = await step(name, async () => {
20957
- await openRow(ROW_ORIGINAL, "\u539F\u521B\u58F0\u660E");
20958
- await clickSelector(page, ORIGINAL_DECLARATION, 5000);
20959
- const agreement = await trySelectors(page, [ORIGINAL_AGREEMENT]);
20960
- if (agreement.found) {
20961
- await clickSelector(page, ORIGINAL_AGREEMENT, 3000);
20962
- }
20963
- await clickSelector(page, DIALOG_CONFIRM, 8000);
20964
- });
20965
- if (outcome !== "capability" && await readChecked(page, COMMITTED.original) !== true && !await residual(name)) {
20966
- warnings.push(`${name}: \u539F\u521B\u58F0\u660E\u672A\u751F\u6548\uFF0C\u8BF7\u624B\u52A8\u8BBE\u7F6E`);
20967
- }
20968
- }
20969
- }
20970
- if (on("reward")) {
20971
- const name = "\u8D5E\u8D4F";
20972
- if (await readChecked(page, COMMITTED.reward) !== true) {
20973
- const outcome = await step(name, async () => {
20974
- await openRow(ROW_REWARD, "\u8D5E\u8D4F");
20975
- const blocked = await tryFindSelector(page, [`${DIALOG} :has-text("\u524D\u5F80\u5F00\u901A")`, `${DIALOG} :has-text("\u65E0\u6CD5\u5F00\u542F")`], 1500);
20976
- if (blocked !== undefined)
20977
- throw new CapabilityError("\u9700\u8981\u8D5E\u8D4F\u8D26\u6237\uFF08\u8FD0\u8425\u8005\u5148\u5728\u5E73\u53F0\u5F00\u901A\uFF09\uFF0C\u5DF2\u8DF3\u8FC7\u81EA\u52A8\u586B\u5199");
20978
- const author = str("author");
20979
- if (author !== "") {
20980
- for (const selector of REWARD_ACCOUNT) {
20981
- const found = await trySelectors(page, [selector]);
20982
- if (!found.found)
20983
- continue;
20984
- const locator = page.locator(selector).first();
20985
- if (typeof locator.inputValue !== "function")
20986
- break;
20987
- const empty = norm(await locator.inputValue().catch(() => null)) === "";
20988
- if (empty)
20989
- await fillSelector(page, selector, author);
20990
- break;
20991
- }
20992
- }
20993
- await clickSelector(page, DIALOG_CONFIRM, 8000);
20994
- });
20995
- if (outcome !== "capability" && await readChecked(page, COMMITTED.reward) !== true && !await residual(name)) {
20996
- warnings.push(`${name}: \u8D5E\u8D4F\u672A\u751F\u6548\uFF0C\u8BF7\u624B\u52A8\u8BBE\u7F6E`);
20997
- }
20998
- }
20999
- }
21000
- const collection = str("collection");
21001
- if (collection !== "") {
21002
- const name = "\u5408\u96C6";
21003
- await step(name, async () => {
21004
- await openRow(ROW_COLLECTION, "\u5408\u96C6");
21005
- await waitForVisible(page, TAG_INPUT, 8000);
21006
- await fillSelector(page, TAG_INPUT, collection);
21007
- await page.waitForTimeout?.(COLLECTION_SETTLE_MS);
21008
- await clickSelector(page, `${DIALOG} :text("${collection}")`, 8000);
21009
- await clickSelector(page, DIALOG_CONFIRM, 8000);
21010
- });
21011
- await residual(name);
21012
- }
21013
- const originalUrl = str("originalUrl");
21014
- if (originalUrl !== "") {
21015
- const name = "\u539F\u6587\u94FE\u63A5";
21016
- const urlRow = await tryFindSelector(page, ROW_URL, 2000);
21017
- if (urlRow === undefined) {
21018
- warnings.push(`${name}: \u884C\u4E0D\u5B58\u5728\uFF08\u5F53\u524D\u516C\u4F17\u53F7\u4E0D\u652F\u6301\uFF09\uFF0C\u8BF7\u624B\u52A8\u5904\u7406\u6216\u79FB\u9664 originalUrl`);
21019
- } else {
21020
- await step(name, async () => {
21021
- await openRow(ROW_URL, "\u539F\u6587\u94FE\u63A5");
21022
- const field = await trySelectors(page, URL_INPUT);
21023
- if (!field.found)
21024
- throw new Error("no URL field in the \u539F\u6587\u94FE\u63A5 dialog");
21025
- await fillSelector(page, field.selector ?? URL_INPUT.join(", "), originalUrl);
21026
- await clickSelector(page, DIALOG_CONFIRM, 8000);
21027
- });
21028
- await residual(name);
21029
- const locator = page.locator(URL_INPUT[0] ?? "").first();
21030
- const committed = typeof locator.inputValue === "function" ? await locator.inputValue().catch(() => null) : null;
21031
- if (norm(committed) !== norm(originalUrl))
21032
- warnings.push(`${name}: \u5DF2\u63D0\u4EA4\u4F46\u672A\u8BFB\u53D6\u5230\u5DF2\u4FDD\u5B58\u7684\u94FE\u63A5\uFF0C\u8BF7\u6838\u5BF9`);
21033
- }
21034
- }
21035
- const claimSource = str("claimSource");
21036
- if (claimSource !== "") {
21037
- const name = "\u521B\u4F5C\u6765\u6E90";
21038
- const claimMatched = async () => {
21039
- try {
21040
- const raw = await page.evaluate(CLAIM_COMMITTED);
21041
- if (raw === null || typeof raw !== "object")
21042
- return null;
21043
- return raw.enabled === true && norm(raw.text) === norm(claimSource);
21044
- } catch {
21045
- return null;
21046
- }
21047
- };
21048
- if (await claimMatched() !== true) {
21049
- await step(name, async () => {
21050
- await openRow(ROW_CLAIM, "\u521B\u4F5C\u6765\u6E90");
21051
- const dialog = await waitForVisible(page, DIALOG, 3000, { required: false });
21052
- if (dialog !== undefined) {
21053
- await clickSelector(page, `${DIALOG} :text("${claimSource}")`, 8000);
21054
- await clickSelector(page, DIALOG_CONFIRM, 8000);
21055
- }
21056
- });
21057
- }
21058
- if (await claimMatched() !== true && !await residual(name)) {
21059
- warnings.push(`${name}: \u672A\u751F\u6548\uFF0C\u8BF7\u624B\u52A8\u8BBE\u7F6E`);
21060
- }
21061
- }
21062
- await sweep();
21063
- return warnings;
21154
+ crops.forEach((crop, index) => {
21155
+ fields[`size${index}_x1`] = String(crop.x1);
21156
+ fields[`size${index}_y1`] = String(crop.y1);
21157
+ fields[`size${index}_x2`] = String(crop.x2);
21158
+ fields[`size${index}_y2`] = String(crop.y2);
21159
+ });
21160
+ const { text } = await postCgiForm(page, {
21161
+ url: buildUrl(CROPIMAGE_URL, { action: "crop_multi" }),
21162
+ fields,
21163
+ maxBytes: MAX_RESPONSE_BYTES,
21164
+ timeoutMs: REQUEST_TIMEOUT_MS
21165
+ });
21166
+ return parseCropMultiReceipt(text, crops);
21167
+ }
21168
+ async function uploadAndCropCover(page, session, input) {
21169
+ const uploaded = await uploadCoverImage(page, session, input);
21170
+ const crops = [
21171
+ centerCropRect(uploaded.width, uploaded.height, WIDE_COVER_ASPECT),
21172
+ centerCropRect(uploaded.width, uploaded.height, SQUARE_COVER_ASPECT)
21173
+ ];
21174
+ const [wide, square] = await cropCoverMulti(page, session, uploaded.url, crops);
21175
+ return { wide, square, crops };
21064
21176
  }
21065
21177
 
21066
21178
  // ../../plugins/publishings/wechatmp-pub/src/modes.ts
@@ -21164,156 +21276,1132 @@ async function resolveBodyImages(content) {
21164
21276
  if (typeof path !== "string" || path.length === 0) {
21165
21277
  return { error: `images validation: images[${index}] carries no path` };
21166
21278
  }
21167
- candidates.push({ path, what: `metadata.images[${index}]` });
21279
+ candidates.push({ path, what: `metadata.images[${index}]` });
21280
+ }
21281
+ const paths = [];
21282
+ for (const { path, what } of candidates) {
21283
+ if (!isAbsolute2(path)) {
21284
+ return { error: `images validation: ${what} must be absolute, got '${path}'` };
21285
+ }
21286
+ if (!IMAGE_EXTENSIONS.test(path)) {
21287
+ return {
21288
+ error: `images validation: ${what} has unsupported extension (expected ${IMAGE_LIST}): '${path}'`
21289
+ };
21290
+ }
21291
+ if (!await createNodeFileSystem().exists(path)) {
21292
+ return { error: `images validation: ${what} does not exist: '${path}'` };
21293
+ }
21294
+ paths.push(path);
21295
+ }
21296
+ return { paths };
21297
+ }
21298
+ function deriveDigest(content) {
21299
+ const explicit = content.metadata?.digest;
21300
+ if (typeof explicit === "string")
21301
+ return explicit;
21302
+ const body = content.body ?? "";
21303
+ const first = body.split(`
21304
+ `).map((line) => line.trim()).find((line) => line !== "");
21305
+ return first === undefined ? undefined : first.slice(0, DERIVED_DIGEST_MAX);
21306
+ }
21307
+ function validatePublishStatus(content) {
21308
+ const status = content.metadata?.publishStatus;
21309
+ if (status !== undefined && status !== "draft" && status !== "public") {
21310
+ return {
21311
+ error: `publishStatus validation: unknown publishStatus '${String(status)}' (expected 'draft' or 'public')`
21312
+ };
21313
+ }
21314
+ return {};
21315
+ }
21316
+ function validateTitle(content, operation) {
21317
+ const limits = MODE_LIMITS[operation];
21318
+ const title = content.title ?? "";
21319
+ if (limits.titleRequired === true && title.trim().length === 0) {
21320
+ return {
21321
+ error: `title validation: Content.title is required for ${operation} (the editor's placeholder is \u8BF7\u5728\u8FD9\u91CC\u8F93\u5165\u6807\u9898)`
21322
+ };
21323
+ }
21324
+ if (limits.titleMax !== undefined && title.length > limits.titleMax) {
21325
+ return {
21326
+ error: `title validation: Content.title is ${title.length} chars but ${operation} accepts \u2264${limits.titleMax}`
21327
+ };
21328
+ }
21329
+ return {};
21330
+ }
21331
+ function validateDigest(content, operation) {
21332
+ const max = MODE_LIMITS[operation].digestMax;
21333
+ if (max === undefined)
21334
+ return {};
21335
+ const digest = deriveDigest(content);
21336
+ if (digest !== undefined && digest.length > max) {
21337
+ return { error: `digest validation: the \u6458\u8981 is ${digest.length} chars but ${operation} accepts \u2264${max}` };
21338
+ }
21339
+ return digest === undefined ? {} : { digest };
21340
+ }
21341
+ function validateAuthor(content, operation) {
21342
+ const max = MODE_LIMITS[operation].authorMax;
21343
+ const author = content.metadata?.author;
21344
+ if (typeof author !== "string" || author.length === 0)
21345
+ return {};
21346
+ if (max !== undefined && author.length > max) {
21347
+ return {
21348
+ error: `author validation: Content.metadata.author is ${author.length} chars but ${operation} accepts \u2264${max}`
21349
+ };
21350
+ }
21351
+ return { author };
21352
+ }
21353
+ async function validateModeInput(operation, content) {
21354
+ const status = validatePublishStatus(content);
21355
+ if (status.error)
21356
+ return { error: status.error };
21357
+ const title = validateTitle(content, operation);
21358
+ if (title.error)
21359
+ return { error: title.error };
21360
+ const author = validateAuthor(content, operation);
21361
+ if (author.error)
21362
+ return { error: author.error };
21363
+ switch (operation) {
21364
+ case "\u6587\u7AE0": {
21365
+ if ((content.body ?? "").trim().length === 0) {
21366
+ return { error: "body validation: Content.body is required for \u6587\u7AE0" };
21367
+ }
21368
+ const digest = validateDigest(content, operation);
21369
+ if (digest.error)
21370
+ return { error: digest.error };
21371
+ const images = await resolveBodyImages(content);
21372
+ if (images.error)
21373
+ return { error: images.error };
21374
+ if ((images.paths ?? []).length === 0) {
21375
+ return {
21376
+ error: "cover validation: \u6587\u7AE0 takes its cover from a body image (\u9ED8\u8BA4\u9996\u56FE\u4E3A\u5C01\u9762) but none was supplied \u2014 set metadata.wechatmp.coverPath or metadata.images[]"
21377
+ };
21378
+ }
21379
+ return {
21380
+ input: {
21381
+ operation,
21382
+ images: images.paths ?? [],
21383
+ ...digest.digest === undefined ? {} : { digest: digest.digest }
21384
+ }
21385
+ };
21386
+ }
21387
+ case "\u8D34\u56FE": {
21388
+ const images = await resolveBodyImages(content);
21389
+ if (images.error)
21390
+ return { error: images.error };
21391
+ if ((images.paths ?? []).length === 0) {
21392
+ return {
21393
+ error: "image validation: \u8D34\u56FE requires at least one image (metadata.images[] or metadata.wechatmp.coverPath)"
21394
+ };
21395
+ }
21396
+ return { input: { operation, images: images.paths ?? [] } };
21397
+ }
21398
+ case "\u89C6\u9891": {
21399
+ const video = await resolveMediaPath(content, {
21400
+ key: "video",
21401
+ label: "video validation",
21402
+ extensions: VIDEO_EXTENSIONS,
21403
+ list: VIDEO_LIST
21404
+ });
21405
+ if (video.error)
21406
+ return { error: video.error };
21407
+ return { input: { operation, video: video.path ?? "" } };
21408
+ }
21409
+ case "\u64AD\u5BA2": {
21410
+ const audio = await resolveMediaPath(content, {
21411
+ key: "audio",
21412
+ label: "audio validation",
21413
+ extensions: AUDIO_EXTENSIONS,
21414
+ list: AUDIO_LIST
21415
+ });
21416
+ if (audio.error)
21417
+ return { error: audio.error };
21418
+ const cover = await resolveMediaPath(content, {
21419
+ key: "coverImage",
21420
+ label: "cover validation",
21421
+ extensions: IMAGE_EXTENSIONS,
21422
+ list: IMAGE_LIST
21423
+ });
21424
+ if (cover.error)
21425
+ return { error: cover.error };
21426
+ return { input: { operation, audio: audio.path ?? "", cover: cover.path ?? "" } };
21427
+ }
21428
+ }
21429
+ }
21430
+
21431
+ // ../../plugins/publishings/wechatmp-pub/src/cgi-draft.ts
21432
+ var MP_ORIGIN2 = "https://mp.weixin.qq.com";
21433
+ var CREATE_URL = `${MP_ORIGIN2}/cgi-bin/operate_appmsg`;
21434
+ var APPMSG_URL = `${MP_ORIGIN2}/cgi-bin/appmsg`;
21435
+ var MAX_RESPONSE_BYTES2 = 2 * 1024 * 1024;
21436
+ var REQUEST_TIMEOUT_MS2 = 30000;
21437
+ var MAX_SNAPSHOT_CHARS = 2 * 1024 * 1024;
21438
+ var DEFAULT_OPERATION = "\u6587\u7AE0";
21439
+
21440
+ class WechatCgiDraftError extends Error {
21441
+ code;
21442
+ stage;
21443
+ metadata;
21444
+ constructor(code, stage, message, metadata = {}) {
21445
+ super(message);
21446
+ this.code = code;
21447
+ this.stage = stage;
21448
+ this.metadata = metadata;
21449
+ this.name = "WechatCgiDraftError";
21450
+ }
21451
+ }
21452
+ var ALLOWED_METADATA_KEYS = new Set([
21453
+ "operation",
21454
+ "publishStatus",
21455
+ "title",
21456
+ "body",
21457
+ "digest",
21458
+ "author",
21459
+ "contentId",
21460
+ "source",
21461
+ "sourceUrl",
21462
+ "tags",
21463
+ "categories",
21464
+ "coverPath",
21465
+ "images"
21466
+ ]);
21467
+ function isNonemptyString(value) {
21468
+ return typeof value === "string" && value.length > 0;
21469
+ }
21470
+ function isRecord2(value) {
21471
+ return typeof value === "object" && value !== null;
21472
+ }
21473
+ function certifyCgiDraft(content) {
21474
+ const metadata = content.metadata ?? {};
21475
+ function refuse(detail) {
21476
+ throw new WechatCgiDraftError("ineligible", "eligibility", `content is not eligible for the CGI draft path: ${detail}`);
21477
+ }
21478
+ const operation = metadata.operation ?? DEFAULT_OPERATION;
21479
+ if (operation !== DEFAULT_OPERATION) {
21480
+ refuse(`operation is not ${DEFAULT_OPERATION}`);
21481
+ }
21482
+ if (metadata.publishStatus !== undefined && metadata.publishStatus !== "draft") {
21483
+ refuse("publishStatus is not draft");
21484
+ }
21485
+ if (!isNonemptyString(content.title)) {
21486
+ refuse("title is missing or empty");
21487
+ }
21488
+ if (!isNonemptyString(content.body)) {
21489
+ refuse("body is missing or empty");
21490
+ }
21491
+ for (const key of ["digest", "author"]) {
21492
+ if (metadata[key] !== undefined && typeof metadata[key] !== "string") {
21493
+ refuse(`${key} is present but not a string`);
21494
+ }
21495
+ }
21496
+ for (const key of Object.keys(metadata)) {
21497
+ if (!ALLOWED_METADATA_KEYS.has(key)) {
21498
+ refuse(`metadata key '${key}' is outside the plain-article set`);
21499
+ }
21500
+ }
21501
+ return {
21502
+ operation: DEFAULT_OPERATION,
21503
+ publishStatus: "draft",
21504
+ title: content.title,
21505
+ body: content.body,
21506
+ digest: typeof metadata.digest === "string" ? metadata.digest : "",
21507
+ author: typeof metadata.author === "string" ? metadata.author : ""
21508
+ };
21509
+ }
21510
+ function assertEligibleCgiDraft(content) {
21511
+ certifyCgiDraft(content);
21512
+ }
21513
+ function stripQuery2(detail) {
21514
+ return detail.replace(/\?[^'"\\\s]*/g, "?\u2026");
21515
+ }
21516
+ function assertMpOrigin2(page) {
21517
+ let origin;
21518
+ try {
21519
+ origin = new URL(page.url()).origin;
21520
+ } catch {
21521
+ throw new WechatCgiDraftError("request-failed", "assert-origin", `page URL is not usable for CGI dispatch: ${page.url()}`);
21522
+ }
21523
+ if (origin !== MP_ORIGIN2) {
21524
+ throw new WechatCgiDraftError("request-failed", "assert-origin", `CGI dispatch requires page origin ${MP_ORIGIN2}, got ${origin}`);
21525
+ }
21526
+ }
21527
+ function extractSessionExpression() {
21528
+ return `(function () {
21529
+ const fields = {};
21530
+ const urlToken = new URLSearchParams(location.search).get('token');
21531
+ if (urlToken !== null && /^\\d{4,32}$/.test(urlToken)) fields.token = urlToken;
21532
+ const snapshot = document.documentElement.outerHTML.slice(0, ${MAX_SNAPSHOT_CHARS});
21533
+ const first = (regex) => {
21534
+ const match = regex.exec(snapshot);
21535
+ return match === null ? undefined : match[1];
21536
+ };
21537
+ // URL token wins, but ticket/user_name/uin are scan-only: a URL-token page still needs them,
21538
+ // so only the token lookup is skipped when the URL already supplied it (task 0290 chunk C bugfix).
21539
+ if (!fields.token) {
21540
+ const token = first(/\\btoken\\s*=\\s*['"](\\d{4,32})['"]/);
21541
+ if (token !== undefined) fields.token = token;
21542
+ }
21543
+ const ticketId = first(/\\bticket_id\\s*=\\s*['"]([A-Za-z0-9]{8,128})['"]/);
21544
+ if (ticketId !== undefined) fields.ticketId = ticketId;
21545
+ const ticket = first(/\\bticket\\s*=\\s*['"]([A-Za-z0-9]{8,128})['"]/);
21546
+ if (ticket !== undefined) fields.ticket = ticket;
21547
+ const userName = first(/\\buser_name\\s*=\\s*['"](@[A-Za-z0-9_-]{1,63}|gh_[A-Za-z0-9_-]{1,61})['"]/);
21548
+ if (userName !== undefined) fields.userName = userName;
21549
+ const uin = first(/\\buin\\s*=\\s*['"](\\d{1,32})['"]/);
21550
+ if (uin !== undefined) fields.uin = uin;
21551
+ return JSON.stringify({ kind: 'ok', ...fields });
21552
+ })()`;
21553
+ }
21554
+ function readSessionFields(raw) {
21555
+ if (typeof raw !== "string")
21556
+ return {};
21557
+ let parsed;
21558
+ try {
21559
+ parsed = JSON.parse(raw);
21560
+ } catch {
21561
+ return {};
21562
+ }
21563
+ if (!isRecord2(parsed))
21564
+ return {};
21565
+ const field = (key) => {
21566
+ const value = parsed[key];
21567
+ return typeof value === "string" && value !== "" ? value : undefined;
21568
+ };
21569
+ return {
21570
+ token: field("token"),
21571
+ ticket: field("ticket"),
21572
+ ticketId: field("ticketId"),
21573
+ userName: field("userName"),
21574
+ uin: field("uin")
21575
+ };
21576
+ }
21577
+ async function extractWechatCgiSession(page) {
21578
+ assertMpOrigin2(page);
21579
+ let raw;
21580
+ try {
21581
+ raw = await page.evaluate(extractSessionExpression());
21582
+ } catch (error) {
21583
+ throw new WechatCgiDraftError("session-missing", "extract-session", `session extraction failed inside the console page: ${stripQuery2(String(error))}`);
21584
+ }
21585
+ const fields = readSessionFields(raw);
21586
+ const userName = fields.userName ?? fields.ticketId;
21587
+ const { token, ticket } = fields;
21588
+ if (token === undefined || ticket === undefined || userName === undefined) {
21589
+ const missing = [
21590
+ ...token === undefined ? ["token"] : [],
21591
+ ...ticket === undefined ? ["ticket"] : [],
21592
+ ...userName === undefined ? ["user_name"] : []
21593
+ ];
21594
+ throw new WechatCgiDraftError("session-missing", "extract-session", `authenticated session field(s) missing on the console page: ${missing.join(", ")}`);
21595
+ }
21596
+ return { token, ticket, userName, uin: fields.uin };
21597
+ }
21598
+ function buildUrl2(base, params) {
21599
+ return `${base}?${new URLSearchParams(params).toString()}`;
21600
+ }
21601
+ function randomHex16() {
21602
+ const bytes = crypto.getRandomValues(new Uint8Array(8));
21603
+ let hex = "";
21604
+ for (const byte of bytes)
21605
+ hex += byte.toString(16).padStart(2, "0");
21606
+ return hex;
21607
+ }
21608
+ function buildCreateArticlePayload(session, content, media, opts) {
21609
+ const metadata = content.metadata ?? {};
21610
+ const plain = (key) => {
21611
+ const value = metadata[key];
21612
+ return typeof value === "string" ? value : "";
21613
+ };
21614
+ const fields = {
21615
+ token: session.token,
21616
+ lang: "zh_CN",
21617
+ f: "json",
21618
+ ajax: "1",
21619
+ random: opts?.random ?? randomHex16(),
21620
+ AppMsgId: "",
21621
+ count: "1",
21622
+ data_seq: "0",
21623
+ operate_from: "Chrome",
21624
+ isnew: "0",
21625
+ type: "77",
21626
+ title0: content.title ?? "",
21627
+ author0: plain("author"),
21628
+ digest0: deriveDigest(content) ?? "",
21629
+ auto_gen_digest0: "0",
21630
+ content0: media.html,
21631
+ sourceurl0: "",
21632
+ cdn_url0: media.cover.wide.url,
21633
+ cdn_235_1_url0: media.cover.wide.url,
21634
+ cdn_1_1_url0: media.cover.square.url,
21635
+ cdn_url_back0: media.cover.wide.url,
21636
+ crop_list0: JSON.stringify(media.cover.crops),
21637
+ share_page_type0: "0",
21638
+ share_imageinfo0: JSON.stringify({ list: [] }),
21639
+ categories_list0: "[]",
21640
+ compose_info0: JSON.stringify({ list: "" })
21641
+ };
21642
+ return {
21643
+ url: buildUrl2(CREATE_URL, {
21644
+ t: "ajax-response",
21645
+ sub: "create",
21646
+ type: "77",
21647
+ token: session.token,
21648
+ lang: "zh_CN"
21649
+ }),
21650
+ fields
21651
+ };
21652
+ }
21653
+ function cgiFetchExpression2(spec) {
21654
+ return `(async () => {
21655
+ const spec = ${JSON.stringify(spec)};
21656
+ try {
21657
+ const init = {
21658
+ method: spec.method,
21659
+ credentials: 'include',
21660
+ redirect: 'error',
21661
+ signal: AbortSignal.timeout(spec.timeoutMs),
21662
+ };
21663
+ if (spec.method === 'POST') {
21664
+ const form = new FormData();
21665
+ for (const key of Object.keys(spec.fields)) form.append(key, spec.fields[key]);
21666
+ init.body = form;
21667
+ }
21668
+ const response = await fetch(spec.url, init);
21669
+ if (!response.ok) return JSON.stringify({ kind: 'http-status', status: response.status });
21670
+ if (!response.body) return JSON.stringify({ kind: 'error', detail: 'response has no body stream' });
21671
+ const reader = response.body.getReader();
21672
+ let total = 0;
21673
+ const chunks = [];
21674
+ for (;;) {
21675
+ const { done, value } = await reader.read();
21676
+ if (done) break;
21677
+ total += value.byteLength;
21678
+ if (total > spec.maxBytes) return JSON.stringify({ kind: 'too-large' });
21679
+ chunks.push(value);
21680
+ }
21681
+ const text = await new Blob(chunks).text();
21682
+ return JSON.stringify({ kind: 'ok', status: response.status, text });
21683
+ } catch (error) {
21684
+ return JSON.stringify({ kind: 'error', detail: String(error) });
21685
+ }
21686
+ })()`;
21687
+ }
21688
+ async function evaluateCgiVerdict(page, expression) {
21689
+ const raw = await page.evaluate(expression);
21690
+ if (typeof raw !== "string")
21691
+ return;
21692
+ try {
21693
+ return JSON.parse(raw);
21694
+ } catch {
21695
+ return;
21696
+ }
21697
+ }
21698
+ function ambiguousSubmission(stage, detail) {
21699
+ return new WechatCgiDraftError("ambiguous-submission", stage, detail, { ambiguousSubmission: true });
21700
+ }
21701
+ async function createWechatArticleDraft(page, session, content, media) {
21702
+ assertEligibleCgiDraft(content);
21703
+ assertMpOrigin2(page);
21704
+ const payload = buildCreateArticlePayload(session, content, media);
21705
+ let createVerdict;
21706
+ try {
21707
+ createVerdict = await evaluateCgiVerdict(page, cgiFetchExpression2({
21708
+ url: payload.url,
21709
+ method: "POST",
21710
+ fields: payload.fields,
21711
+ maxBytes: MAX_RESPONSE_BYTES2,
21712
+ timeoutMs: REQUEST_TIMEOUT_MS2
21713
+ }));
21714
+ } catch (error) {
21715
+ throw ambiguousSubmission("create-dispatch", `operate_appmsg create ended before a receipt returned: ${stripQuery2(String(error))}`);
21716
+ }
21717
+ if (createVerdict?.kind === "http-status") {
21718
+ throw ambiguousSubmission("create-dispatch", `operate_appmsg create returned HTTP ${createVerdict.status}`);
21719
+ }
21720
+ if (createVerdict?.kind === "too-large") {
21721
+ throw ambiguousSubmission("create-dispatch", `create response exceeded the ${MAX_RESPONSE_BYTES2}-byte bound`);
21722
+ }
21723
+ if (createVerdict?.kind !== "ok") {
21724
+ const detail = createVerdict?.kind === "error" ? stripQuery2(createVerdict.detail) : "page returned no usable verdict";
21725
+ throw ambiguousSubmission("create-dispatch", `create failed before a usable response: ${detail}`);
21726
+ }
21727
+ let body;
21728
+ try {
21729
+ body = JSON.parse(createVerdict.text);
21730
+ } catch {
21731
+ throw ambiguousSubmission("create-receipt", "create receipt is not JSON");
21732
+ }
21733
+ if (!isRecord2(body)) {
21734
+ throw ambiguousSubmission("create-receipt", "create receipt is not a JSON object");
21735
+ }
21736
+ if (body.ret !== undefined && body.ret !== 0) {
21737
+ throw new WechatCgiDraftError("receipt-invalid", "create-receipt", `operate_appmsg create refused: ret=${String(body.ret)}`);
21738
+ }
21739
+ const receipt = isRecord2(body.appMsgInfo) ? body.appMsgInfo : body;
21740
+ const rawId = receipt.appMsgId;
21741
+ const appMsgId = typeof rawId === "string" || typeof rawId === "number" ? String(rawId) : undefined;
21742
+ if (appMsgId === undefined || !/^-?\d{1,12}$/.test(appMsgId)) {
21743
+ throw ambiguousSubmission("create-receipt", "create receipt did not return a safe numeric appMsgId");
21744
+ }
21745
+ const listUrl = buildUrl2(APPMSG_URL, {
21746
+ action: "list_ex",
21747
+ begin: "0",
21748
+ count: "5",
21749
+ token: session.token,
21750
+ lang: "zh_CN",
21751
+ f: "json"
21752
+ });
21753
+ let listVerdict;
21754
+ try {
21755
+ listVerdict = await evaluateCgiVerdict(page, cgiFetchExpression2({
21756
+ url: listUrl,
21757
+ method: "GET",
21758
+ fields: {},
21759
+ maxBytes: MAX_RESPONSE_BYTES2,
21760
+ timeoutMs: REQUEST_TIMEOUT_MS2
21761
+ }));
21762
+ } catch (error) {
21763
+ throw new WechatCgiDraftError("readback-failed", "readback", `draft list readback ended before a receipt returned: ${stripQuery2(String(error))}`);
21764
+ }
21765
+ if (listVerdict?.kind !== "ok") {
21766
+ const detail = listVerdict?.kind === "http-status" ? `HTTP ${listVerdict.status}` : listVerdict?.kind === "too-large" ? `${MAX_RESPONSE_BYTES2}-byte bound exceeded` : listVerdict?.kind === "error" ? stripQuery2(listVerdict.detail) : "page returned no usable verdict";
21767
+ throw new WechatCgiDraftError("readback-failed", "readback", `draft list readback failed: ${detail}`);
21768
+ }
21769
+ let list;
21770
+ try {
21771
+ list = JSON.parse(listVerdict.text);
21772
+ } catch {
21773
+ throw new WechatCgiDraftError("readback-failed", "readback", "draft list receipt is not JSON");
21774
+ }
21775
+ const items = isRecord2(list) && Array.isArray(list.app_msg_list) ? list.app_msg_list : [];
21776
+ const confirmed = items.some((item) => isRecord2(item) && item.title === content.title && (String(item.app_id) === appMsgId || String(item.appmsgid) === appMsgId));
21777
+ if (!confirmed) {
21778
+ throw new WechatCgiDraftError("readback-failed", "readback", "draft list did not confirm an item matching the created appMsgId and title");
21779
+ }
21780
+ return {
21781
+ appMsgId,
21782
+ editPath: `/cgi-bin/appmsg?t=media/appmsg_edit&action=edit&isNew=0&type=77&appmsgid=${appMsgId}`
21783
+ };
21784
+ }
21785
+
21786
+ // ../../plugins/publishings/wechatmp-pub/src/cgi-media.ts
21787
+ var MP_ORIGIN3 = "https://mp.weixin.qq.com";
21788
+ var UPLOADIMG2CDN_URL = `${MP_ORIGIN3}/cgi-bin/uploadimg2cdn`;
21789
+ var MAX_RESPONSE_BYTES3 = 2 * 1024 * 1024;
21790
+ var REQUEST_TIMEOUT_MS3 = 30000;
21791
+ var TOTAL_DEADLINE_MS = 180000;
21792
+ var MAX_AGGREGATE_BYTES = 20 * 1024 * 1024;
21793
+ var MAX_BODY_SOURCES = 30;
21794
+ var MAX_SOURCE_DETAIL_LENGTH = 120;
21795
+
21796
+ class WechatMediaError extends Error {
21797
+ reason;
21798
+ constructor(reason, message) {
21799
+ super(message);
21800
+ this.reason = reason;
21801
+ this.name = "WechatMediaError";
21802
+ }
21803
+ }
21804
+ function safeSource(source) {
21805
+ const bare = source.replace(/[?#].*$/, "");
21806
+ return bare.length > MAX_SOURCE_DETAIL_LENGTH ? `${bare.slice(0, MAX_SOURCE_DETAIL_LENGTH)}\u2026` : bare;
21807
+ }
21808
+ function stripQuery3(detail) {
21809
+ return detail.replace(/\?[^'"\\\s]*/g, "?\u2026");
21810
+ }
21811
+ function assertMpOrigin3(page) {
21812
+ let origin;
21813
+ try {
21814
+ origin = new URL(page.url()).origin;
21815
+ } catch {
21816
+ throw new WechatMediaError("wrong-origin", `page URL is not usable for media preparation: ${page.url()}`);
21817
+ }
21818
+ if (origin !== MP_ORIGIN3) {
21819
+ throw new WechatMediaError("wrong-origin", `media preparation requires page origin ${MP_ORIGIN3}, got ${origin}`);
21820
+ }
21821
+ }
21822
+ function assertDeadline(deadlineAt) {
21823
+ if (Date.now() > deadlineAt) {
21824
+ throw new WechatMediaError("deadline-exceeded", `article media preparation exceeded the ${TOTAL_DEADLINE_MS}ms deadline`);
21825
+ }
21826
+ }
21827
+ function assertPublicHttpSource(source) {
21828
+ let url;
21829
+ try {
21830
+ url = new URL(source);
21831
+ } catch {
21832
+ throw new WechatMediaError("unmapped-source", `body image source is not absolute and has no localImages mapping: ${safeSource(source)}`);
21833
+ }
21834
+ if (url.protocol !== "https:" && url.protocol !== "http:") {
21835
+ throw new WechatMediaError("unmapped-source", `body image source scheme ${url.protocol} requires a localImages mapping: ${safeSource(source)}`);
21836
+ }
21837
+ if (url.username !== "" || url.password !== "") {
21838
+ throw new WechatMediaError("unmapped-source", `body image source carries credentials and is not transferable: ${safeSource(source)}`);
21839
+ }
21840
+ if (isLocalHostLiteral(url.hostname)) {
21841
+ throw new WechatMediaError("unmapped-source", `body image source is a local-IP literal and is not transferable: ${safeSource(source)}`);
21842
+ }
21843
+ }
21844
+ function isLocalHostLiteral(hostname) {
21845
+ const host = hostname.toLowerCase().replace(/^\[|\]$/g, "");
21846
+ return host === "localhost" || host === "::1" || host === "0.0.0.0" || /^127\./.test(host) || /^10\./.test(host) || /^192\.168\./.test(host) || /^169\.254\./.test(host) || /^172\.(1[6-9]|2\d|3[01])\./.test(host) || /^(f[cd]|fe80)/.test(host);
21847
+ }
21848
+ function parseArticleExpression(html) {
21849
+ return `(function () {
21850
+ const doc = new DOMParser().parseFromString(${JSON.stringify(html)}, 'text/html');
21851
+ const dangerous = (value) => {
21852
+ const compact = String(value).toLowerCase().replace(/\\s+/g, '');
21853
+ return ['javascript:', 'data:', 'vbscript:'].some((scheme) => compact.startsWith(scheme));
21854
+ };
21855
+ doc.body.querySelectorAll('*').forEach((el) => {
21856
+ [...el.attributes].forEach((attr) => {
21857
+ const name = attr.name.toLowerCase();
21858
+ if (name.startsWith('on') || dangerous(attr.value)) el.removeAttribute(attr.name);
21859
+ });
21860
+ });
21861
+ doc.body.querySelectorAll('a[href]').forEach((a) => {
21862
+ const span = doc.createElement('span');
21863
+ while (a.firstChild) span.appendChild(a.firstChild);
21864
+ a.replaceWith(span);
21865
+ });
21866
+ const sources = [];
21867
+ const seen = new Set();
21868
+ doc.body.querySelectorAll('img').forEach((img) => {
21869
+ const src = img.getAttribute('src') || '';
21870
+ if (src !== '' && !seen.has(src)) {
21871
+ seen.add(src);
21872
+ sources.push(src);
21873
+ }
21874
+ });
21875
+ return JSON.stringify({ kind: 'ok', sources, html: doc.body.innerHTML });
21876
+ })()`;
21877
+ }
21878
+ function applyReplacementsExpression(html, replacements) {
21879
+ return `(function () {
21880
+ const doc = new DOMParser().parseFromString(${JSON.stringify(html)}, 'text/html');
21881
+ const map = ${JSON.stringify(replacements)};
21882
+ doc.body.querySelectorAll('img').forEach((img) => {
21883
+ const src = img.getAttribute('src') || '';
21884
+ if (map[src]) img.setAttribute('src', map[src]);
21885
+ });
21886
+ return JSON.stringify({ kind: 'ok', html: doc.body.innerHTML });
21887
+ })()`;
21888
+ }
21889
+ function transferExpression(spec) {
21890
+ return `(async () => {
21891
+ const spec = ${JSON.stringify(spec)};
21892
+ const controller = new AbortController();
21893
+ const timer = setTimeout(() => controller.abort(), ${REQUEST_TIMEOUT_MS3});
21894
+ try {
21895
+ const form = new FormData();
21896
+ for (const key of Object.keys(spec.fields)) form.append(key, spec.fields[key]);
21897
+ const response = await fetch(spec.url, {
21898
+ method: 'POST',
21899
+ body: form,
21900
+ credentials: 'include',
21901
+ redirect: 'error',
21902
+ signal: controller.signal,
21903
+ });
21904
+ if (!response.ok) return JSON.stringify({ kind: 'http-status', status: response.status });
21905
+ if (!response.body) return JSON.stringify({ kind: 'error', detail: 'response has no body stream' });
21906
+ const reader = response.body.getReader();
21907
+ let total = 0;
21908
+ const chunks = [];
21909
+ for (;;) {
21910
+ const { done, value } = await reader.read();
21911
+ if (done) break;
21912
+ total += value.byteLength;
21913
+ if (total > spec.maxBytes) {
21914
+ controller.abort();
21915
+ return JSON.stringify({ kind: 'too-large' });
21916
+ }
21917
+ chunks.push(value);
21918
+ }
21919
+ const text = await new Blob(chunks).text();
21920
+ return JSON.stringify({ kind: 'ok', text });
21921
+ } catch (error) {
21922
+ return JSON.stringify({ kind: 'error', detail: String(error) });
21923
+ } finally {
21924
+ clearTimeout(timer);
21925
+ }
21926
+ })()`;
21927
+ }
21928
+ async function evaluateJson(page, expression, what) {
21929
+ const raw = await page.evaluate(expression);
21930
+ if (typeof raw !== "string") {
21931
+ throw new WechatMediaError("transfer-failed", `${what}: page returned no usable verdict`);
21932
+ }
21933
+ try {
21934
+ return JSON.parse(raw);
21935
+ } catch {
21936
+ throw new WechatMediaError("transfer-failed", `${what}: page verdict is not JSON`);
21937
+ }
21938
+ }
21939
+ function isRecord3(value) {
21940
+ return typeof value === "object" && value !== null;
21941
+ }
21942
+ function parseTransferReceipt(text, source) {
21943
+ let body;
21944
+ try {
21945
+ body = JSON.parse(text);
21946
+ } catch {
21947
+ throw new WechatMediaError("bad-transfer-receipt", `uploadimg2cdn: response is not JSON for ${safeSource(source)}`);
21948
+ }
21949
+ if (!isRecord3(body) || body.errcode !== 0) {
21950
+ throw new WechatMediaError("bad-transfer-receipt", `uploadimg2cdn: errcode=${String(isRecord3(body) ? body.errcode : body)} for ${safeSource(source)}`);
21951
+ }
21952
+ const fileId = body.file_id;
21953
+ const url = body.url;
21954
+ if (typeof fileId !== "string" || fileId === "" || typeof url !== "string" || !isAllowedCdnUrl(url)) {
21955
+ throw new WechatMediaError("bad-transfer-receipt", `uploadimg2cdn: file_id/url missing or outside the allowed HTTPS CDN host for ${safeSource(source)}`);
21956
+ }
21957
+ return { fileId, url, width: 0, height: 0 };
21958
+ }
21959
+ async function transferPublicImage(page, session, source) {
21960
+ const query = new URLSearchParams({
21961
+ lang: "zh_CN",
21962
+ token: session.token,
21963
+ t: String(Math.floor(Math.random() * 1e9))
21964
+ });
21965
+ const verdict = await evaluateJson(page, transferExpression({
21966
+ url: `${UPLOADIMG2CDN_URL}?${query.toString()}`,
21967
+ fields: {
21968
+ imgurl: source,
21969
+ t: "ajax-editor-upload-img",
21970
+ token: session.token,
21971
+ lang: "zh_CN",
21972
+ f: "json",
21973
+ ajax: "1"
21974
+ },
21975
+ maxBytes: MAX_RESPONSE_BYTES3
21976
+ }), "uploadimg2cdn");
21977
+ if (verdict.kind === "http-status") {
21978
+ throw new WechatMediaError("transfer-failed", `uploadimg2cdn returned HTTP ${verdict.status}`);
21979
+ }
21980
+ if (verdict.kind === "too-large") {
21981
+ throw new WechatMediaError("transfer-failed", `uploadimg2cdn response exceeded the ${MAX_RESPONSE_BYTES3}-byte bound`);
21982
+ }
21983
+ if (verdict.kind !== "ok" || typeof verdict.text !== "string") {
21984
+ const detail = verdict.kind === "error" ? stripQuery3(verdict.detail) : "page returned no usable verdict";
21985
+ throw new WechatMediaError("transfer-failed", `uploadimg2cdn failed before a usable response: ${detail}`);
21986
+ }
21987
+ return parseTransferReceipt(verdict.text, source);
21988
+ }
21989
+ async function prepareArticleMedia(page, session, input) {
21990
+ assertMpOrigin3(page);
21991
+ const deadlineAt = Date.now() + TOTAL_DEADLINE_MS;
21992
+ const parsed = await evaluateJson(page, parseArticleExpression(input.html), "article body parse");
21993
+ if (parsed.kind !== "ok" || !Array.isArray(parsed.sources) || typeof parsed.html !== "string") {
21994
+ throw new WechatMediaError("transfer-failed", "article body parse did not return a usable verdict");
21995
+ }
21996
+ if (parsed.sources.length > MAX_BODY_SOURCES) {
21997
+ throw new WechatMediaError("too-many-sources", `article body has ${parsed.sources.length} distinct image sources, bound is ${MAX_BODY_SOURCES}`);
21998
+ }
21999
+ assertDeadline(deadlineAt);
22000
+ const cover = await uploadAndCropCover(page, session, { path: input.coverPath });
22001
+ const replacements = {};
22002
+ const images = [];
22003
+ let aggregateBytes = 0;
22004
+ for (const source of parsed.sources) {
22005
+ assertDeadline(deadlineAt);
22006
+ if (isAllowedCdnUrl(source))
22007
+ continue;
22008
+ const mappedPath = input.localImages[source];
22009
+ let uploaded;
22010
+ if (mappedPath !== undefined) {
22011
+ const image = await readLocalImageBytes(mappedPath);
22012
+ aggregateBytes += image.bytes.byteLength;
22013
+ if (aggregateBytes > MAX_AGGREGATE_BYTES) {
22014
+ throw new WechatMediaError("aggregate-too-large", `local body images exceed the ${MAX_AGGREGATE_BYTES}-byte aggregate bound`);
22015
+ }
22016
+ uploaded = await uploadCoverImage(page, session, { path: mappedPath });
22017
+ } else {
22018
+ assertPublicHttpSource(source);
22019
+ uploaded = await transferPublicImage(page, session, source);
22020
+ }
22021
+ replacements[source] = uploaded.url;
22022
+ images.push(uploaded);
22023
+ }
22024
+ const applied = await evaluateJson(page, applyReplacementsExpression(parsed.html, replacements), "CDN replacement");
22025
+ if (applied.kind !== "ok" || typeof applied.html !== "string") {
22026
+ throw new WechatMediaError("transfer-failed", "CDN replacement did not return a usable verdict");
22027
+ }
22028
+ return { html: applied.html, cover, images };
22029
+ }
22030
+
22031
+ // ../../plugins/publishings/wechatmp-pub/src/page.ts
22032
+ var POLL_MS = 500;
22033
+ var STEP_TIMEOUT_MS = 30000;
22034
+ async function modalState(page) {
22035
+ try {
22036
+ const raw = await page.evaluate(`[...document.querySelectorAll('.weui-desktop-dialog__wrp, .weui-desktop-dialog')].some((d) => d.offsetWidth)`);
22037
+ return raw === true ? "open" : raw === false ? "closed" : "unknown";
22038
+ } catch {
22039
+ return "unknown";
22040
+ }
22041
+ }
22042
+ async function tryFindAttachedSelector(page, selectors, timeoutMs = STEP_TIMEOUT_MS) {
22043
+ let found;
22044
+ await waitUntil(async () => {
22045
+ for (const selector of selectors) {
22046
+ const attached = await page.locator(selector).count().catch(() => 0);
22047
+ if (attached > 0) {
22048
+ found = selector;
22049
+ return true;
22050
+ }
22051
+ }
22052
+ return false;
22053
+ }, timeoutMs);
22054
+ return found;
22055
+ }
22056
+ async function tryFindSelector(page, selectors, timeoutMs = STEP_TIMEOUT_MS) {
22057
+ let found;
22058
+ await waitUntil(async () => {
22059
+ const result = await trySelectors(page, selectors).catch(() => ({ found: false, selector: null }));
22060
+ if (result.found && result.selector) {
22061
+ found = result.selector;
22062
+ return true;
22063
+ }
22064
+ return false;
22065
+ }, timeoutMs);
22066
+ return found;
22067
+ }
22068
+ async function findSelector(page, selectors, what, timeoutMs = STEP_TIMEOUT_MS) {
22069
+ const found = await tryFindSelector(page, selectors, timeoutMs);
22070
+ if (found === undefined) {
22071
+ throw new Error(`${what}: none of [${selectors.join(" | ")}] visible within ${timeoutMs}ms`);
22072
+ }
22073
+ return found;
22074
+ }
22075
+ async function findAttachedSelector(page, selectors, what, timeoutMs = STEP_TIMEOUT_MS) {
22076
+ const found = await tryFindAttachedSelector(page, selectors, timeoutMs);
22077
+ if (found === undefined) {
22078
+ throw new Error(`${what}: none of [${selectors.join(" | ")}] attached within ${timeoutMs}ms`);
22079
+ }
22080
+ return found;
22081
+ }
22082
+ async function followNewPage(pages, before, timeoutMs) {
22083
+ const opened = await waitUntil(async () => pages().length > before, timeoutMs);
22084
+ if (!opened)
22085
+ return;
22086
+ const all = pages();
22087
+ return all[all.length - 1];
22088
+ }
22089
+ async function clickSelector(page, selector, timeoutMs = STEP_TIMEOUT_MS) {
22090
+ await page.locator(selector).first().click({ timeout: timeoutMs });
22091
+ }
22092
+ async function fillSelector(page, selector, value) {
22093
+ await page.locator(selector).first().fill(value);
22094
+ }
22095
+ function wrapPlaywrightPage(raw) {
22096
+ const pw = raw;
22097
+ return {
22098
+ goto: (url, options) => pw.goto(url, { waitUntil: "domcontentloaded", ...options }).then(() => {
22099
+ return;
22100
+ }),
22101
+ locator: (selector) => ({
22102
+ count: () => pw.locator(selector).count(),
22103
+ first: () => {
22104
+ const loc = pw.locator(selector).first();
22105
+ return {
22106
+ count: () => loc.count(),
22107
+ isVisible: (options) => loc.isVisible(options),
22108
+ fill: (text, options) => loc.fill(text, options),
22109
+ click: (options) => loc.click(options),
22110
+ type: (text, options) => loc.pressSequentially(text, options),
22111
+ waitFor: (options) => loc.waitFor(options),
22112
+ textContent: async () => await loc.textContent() ?? "",
22113
+ inputValue: () => loc.inputValue()
22114
+ };
22115
+ }
22116
+ }),
22117
+ setInputFiles: (selector, paths) => pw.setInputFiles(selector, paths),
22118
+ url: () => pw.url(),
22119
+ evaluate: (expression) => pw.evaluate(expression),
22120
+ waitForTimeout: (ms) => pw.waitForTimeout(ms),
22121
+ press: (key) => pw.keyboard.press(key),
22122
+ frames: () => {
22123
+ const source = raw;
22124
+ return (source.frames?.() ?? []).map((frame) => ({
22125
+ url: () => frame.url(),
22126
+ locator: (selector) => ({ count: () => frame.locator(selector).count() })
22127
+ }));
22128
+ }
22129
+ };
22130
+ }
22131
+ function wrapPlaywrightContext(raw, entryPage) {
22132
+ const context = raw;
22133
+ const cache = new WeakMap;
22134
+ const wrap = (page) => {
22135
+ const key = page;
22136
+ const cached = cache.get(key);
22137
+ if (cached !== undefined)
22138
+ return cached;
22139
+ const wrapped = wrapPlaywrightPage(page);
22140
+ cache.set(key, wrapped);
22141
+ return wrapped;
22142
+ };
22143
+ return {
22144
+ page: wrap(entryPage),
22145
+ pages: () => context.pages().map(wrap),
22146
+ newPage: async () => wrap(await context.newPage()),
22147
+ close: () => context.close()
22148
+ };
22149
+ }
22150
+ async function waitForVisible(page, selector, timeoutMs, opts = {}) {
22151
+ const visible = await waitUntil(async () => {
22152
+ const found = await trySelectors(page, [selector]).catch(() => ({ found: false, selector: null }));
22153
+ return found.found;
22154
+ }, timeoutMs, POLL_MS);
22155
+ if (visible)
22156
+ return selector;
22157
+ if (opts.required === false)
22158
+ return;
22159
+ throw new Error(`${selector}: not visible within ${timeoutMs}ms`);
22160
+ }
22161
+
22162
+ // ../../plugins/publishings/wechatmp-pub/src/enrich.ts
22163
+ var DIALOG = ".weui-desktop-dialog:visible";
22164
+ var CONFIRM = `${DIALOG} button:has-text("\u786E\u8BA4"), ${DIALOG} button:has-text("\u786E\u5B9A")`;
22165
+ var ROW_ORIGINAL = [
22166
+ ".setting-group__switch.js_original_apply.js_edit_ori",
22167
+ ".setting-group__switch.js_edit_ori",
22168
+ ".js_edit_ori",
22169
+ "text=\u539F\u521B"
22170
+ ];
22171
+ var ROW_REWARD = [
22172
+ ".setting-group__switch.js_reward_open",
22173
+ ".js_reward_open",
22174
+ ".js_reward_allow_click",
22175
+ ".setting-group__switch.js_can_reward",
22176
+ "text=\u8D5E\u8D4F"
22177
+ ];
22178
+ var ROW_COLLECTION = [".allow_click_opr.js_article_tags_label", "text=\u5408\u96C6"];
22179
+ var ROW_URL = [".allow_click_opr.js_url_checkbox", "text=\u539F\u6587\u94FE\u63A5"];
22180
+ var ROW_CLAIM = ["#js_claim_source_area .frm_checkbox_label", "text=\u521B\u4F5C\u6765\u6E90"];
22181
+ var DIALOG_CONFIRM = CONFIRM;
22182
+ var TAG_INPUT = 'input[placeholder*="\u5408\u96C6"]';
22183
+ var REWARD_ACCOUNT = [
22184
+ `${DIALOG} input[placeholder*="\u8D5E\u8D4F"]`,
22185
+ `${DIALOG} input[placeholder*="\u8D26\u6237"]`,
22186
+ `${DIALOG} input.frm_input`
22187
+ ];
22188
+ var URL_INPUT = [`${DIALOG} input[type="url"]`, `${DIALOG} input[placeholder*="\u94FE\u63A5"]`, `${DIALOG} input.frm_input`];
22189
+ var ORIGINAL_DECLARATION = `${DIALOG} label:has(.js_original_type_radio):has-text("\u6587\u5B57\u539F\u521B")`;
22190
+ var ORIGINAL_AGREEMENT = `${DIALOG} .original_agreement`;
22191
+ var COLLECTION_SETTLE_MS = 1200;
22192
+ var STEP_RETRY_SETTLE_MS = 1000;
22193
+ var SWEEP_SETTLE_MS = 400;
22194
+
22195
+ class CapabilityError extends Error {
22196
+ }
22197
+ function norm(value) {
22198
+ return typeof value === "string" ? value.replace(/\s+/g, " ").trim() : "";
22199
+ }
22200
+ var COMMITTED = {
22201
+ original: `(() => {
22202
+ const el = document.querySelector('#js_original, .js_ori_setting_checkbox');
22203
+ return el ? el.checked === true : null;
22204
+ })()`,
22205
+ reward: `(() => {
22206
+ const el = document.querySelector('#js_reward_setting_area .js_reward_setting_checkbox') ||
22207
+ document.querySelector('.js_reward_allow_click_cell .js_reward_setting');
22208
+ return el ? el.checked === true : null;
22209
+ })()`
22210
+ };
22211
+ var CLAIM_COMMITTED = `(() => {
22212
+ const area = document.querySelector('#js_claim_source_area');
22213
+ if (!area) return null;
22214
+ const label = area.querySelector('.js_claim_source_selected');
22215
+ const text = (label ? (label.textContent ?? '') : '').replace(/\\s+/g, ' ').trim();
22216
+ const checkbox = area.querySelector('.js_claim_source, input[type=checkbox]');
22217
+ const enabled = checkbox ? checkbox.checked === true : null;
22218
+ return { enabled, text };
22219
+ })()`;
22220
+ async function readChecked(page, expression) {
22221
+ try {
22222
+ const raw = await page.evaluate(expression);
22223
+ return typeof raw === "boolean" ? raw : null;
22224
+ } catch {
22225
+ return null;
22226
+ }
22227
+ }
22228
+ async function enrichArticle(page, meta) {
22229
+ const warnings = [];
22230
+ const str = (key) => typeof meta[key] === "string" ? meta[key] : "";
22231
+ const on = (key) => meta[key] === true;
22232
+ const sweep = async () => {
22233
+ const cancel = await trySelectors(page, ['button:has-text("\u53D6\u6D88")']);
22234
+ if (cancel.found) {
22235
+ try {
22236
+ await page.locator(cancel.selector ?? "").first().click({ timeout: 2000 });
22237
+ } catch {}
22238
+ }
22239
+ await page.press?.("Escape");
22240
+ await page.waitForTimeout?.(SWEEP_SETTLE_MS);
22241
+ const state = await modalState(page);
22242
+ if (state === "open") {
22243
+ for (const close of [".weui-desktop-dialog__close-btn", ".weui-desktop-dialog__close"]) {
22244
+ const control = await tryFindSelector(page, [close], 1500);
22245
+ if (control === undefined)
22246
+ continue;
22247
+ try {
22248
+ await clickSelector(page, control, 2000);
22249
+ } catch {}
22250
+ break;
22251
+ }
22252
+ return await modalState(page) === "closed" ? "closed" : "unknown";
22253
+ }
22254
+ return state === "closed" ? "closed" : "unknown";
22255
+ };
22256
+ const openRow = async (row, what) => {
22257
+ for (const selector of row) {
22258
+ const found = await trySelectors(page, [selector]);
22259
+ if (found.found) {
22260
+ await clickSelector(page, selector, 8000);
22261
+ return;
22262
+ }
22263
+ }
22264
+ throw new Error(`no ${what} row control among [${row.join(" | ")}]`);
22265
+ };
22266
+ const step = async (name, fn) => {
22267
+ try {
22268
+ await fn();
22269
+ return "ok";
22270
+ } catch (error) {
22271
+ if (error instanceof CapabilityError) {
22272
+ warnings.push(`${name}: ${error.message}`);
22273
+ return "capability";
22274
+ }
22275
+ }
22276
+ await sweep();
22277
+ await page.waitForTimeout?.(STEP_RETRY_SETTLE_MS);
22278
+ try {
22279
+ await fn();
22280
+ return "ok";
22281
+ } catch (error) {
22282
+ warnings.push(`${name}: ${(error instanceof Error ? error.message : String(error)).slice(0, 120)}`);
22283
+ await sweep();
22284
+ return "failed";
22285
+ }
22286
+ };
22287
+ const residual = async (name) => {
22288
+ const state = await modalState(page);
22289
+ if (state === "closed")
22290
+ return false;
22291
+ warnings.push(state === "open" ? `${name}: dialog did not close \u2014 check the draft` : `${name}: dialog state unknown after cleanup \u2014 a residual overlay may block later steps`);
22292
+ return true;
22293
+ };
22294
+ if (on("original")) {
22295
+ const name = "\u539F\u521B\u58F0\u660E";
22296
+ if (await readChecked(page, COMMITTED.original) !== true) {
22297
+ const outcome = await step(name, async () => {
22298
+ await openRow(ROW_ORIGINAL, "\u539F\u521B\u58F0\u660E");
22299
+ await clickSelector(page, ORIGINAL_DECLARATION, 5000);
22300
+ const agreement = await trySelectors(page, [ORIGINAL_AGREEMENT]);
22301
+ if (agreement.found) {
22302
+ await clickSelector(page, ORIGINAL_AGREEMENT, 3000);
22303
+ }
22304
+ await clickSelector(page, DIALOG_CONFIRM, 8000);
22305
+ });
22306
+ if (outcome !== "capability" && await readChecked(page, COMMITTED.original) !== true && !await residual(name)) {
22307
+ warnings.push(`${name}: \u539F\u521B\u58F0\u660E\u672A\u751F\u6548\uFF0C\u8BF7\u624B\u52A8\u8BBE\u7F6E`);
22308
+ }
22309
+ }
21168
22310
  }
21169
- const paths = [];
21170
- for (const { path, what } of candidates) {
21171
- if (!isAbsolute2(path)) {
21172
- return { error: `images validation: ${what} must be absolute, got '${path}'` };
21173
- }
21174
- if (!IMAGE_EXTENSIONS.test(path)) {
21175
- return {
21176
- error: `images validation: ${what} has unsupported extension (expected ${IMAGE_LIST}): '${path}'`
21177
- };
21178
- }
21179
- if (!await createNodeFileSystem().exists(path)) {
21180
- return { error: `images validation: ${what} does not exist: '${path}'` };
22311
+ if (on("reward")) {
22312
+ const name = "\u8D5E\u8D4F";
22313
+ if (await readChecked(page, COMMITTED.reward) !== true) {
22314
+ const outcome = await step(name, async () => {
22315
+ await openRow(ROW_REWARD, "\u8D5E\u8D4F");
22316
+ const blocked = await tryFindSelector(page, [`${DIALOG} :has-text("\u524D\u5F80\u5F00\u901A")`, `${DIALOG} :has-text("\u65E0\u6CD5\u5F00\u542F")`], 1500);
22317
+ if (blocked !== undefined)
22318
+ throw new CapabilityError("\u9700\u8981\u8D5E\u8D4F\u8D26\u6237\uFF08\u8FD0\u8425\u8005\u5148\u5728\u5E73\u53F0\u5F00\u901A\uFF09\uFF0C\u5DF2\u8DF3\u8FC7\u81EA\u52A8\u586B\u5199");
22319
+ const author = str("author");
22320
+ if (author !== "") {
22321
+ for (const selector of REWARD_ACCOUNT) {
22322
+ const found = await trySelectors(page, [selector]);
22323
+ if (!found.found)
22324
+ continue;
22325
+ const locator = page.locator(selector).first();
22326
+ if (typeof locator.inputValue !== "function")
22327
+ break;
22328
+ const empty = norm(await locator.inputValue().catch(() => null)) === "";
22329
+ if (empty)
22330
+ await fillSelector(page, selector, author);
22331
+ break;
22332
+ }
22333
+ }
22334
+ await clickSelector(page, DIALOG_CONFIRM, 8000);
22335
+ });
22336
+ if (outcome !== "capability" && await readChecked(page, COMMITTED.reward) !== true && !await residual(name)) {
22337
+ warnings.push(`${name}: \u8D5E\u8D4F\u672A\u751F\u6548\uFF0C\u8BF7\u624B\u52A8\u8BBE\u7F6E`);
22338
+ }
21181
22339
  }
21182
- paths.push(path);
21183
- }
21184
- return { paths };
21185
- }
21186
- function deriveDigest(content) {
21187
- const explicit = content.metadata?.digest;
21188
- if (typeof explicit === "string")
21189
- return explicit;
21190
- const body = content.body ?? "";
21191
- const first = body.split(`
21192
- `).map((line) => line.trim()).find((line) => line !== "");
21193
- return first === undefined ? undefined : first.slice(0, DERIVED_DIGEST_MAX);
21194
- }
21195
- function validatePublishStatus(content) {
21196
- const status = content.metadata?.publishStatus;
21197
- if (status !== undefined && status !== "draft" && status !== "public") {
21198
- return {
21199
- error: `publishStatus validation: unknown publishStatus '${String(status)}' (expected 'draft' or 'public')`
21200
- };
21201
- }
21202
- return {};
21203
- }
21204
- function validateTitle(content, operation) {
21205
- const limits = MODE_LIMITS[operation];
21206
- const title = content.title ?? "";
21207
- if (limits.titleRequired === true && title.trim().length === 0) {
21208
- return {
21209
- error: `title validation: Content.title is required for ${operation} (the editor's placeholder is \u8BF7\u5728\u8FD9\u91CC\u8F93\u5165\u6807\u9898)`
21210
- };
21211
- }
21212
- if (limits.titleMax !== undefined && title.length > limits.titleMax) {
21213
- return {
21214
- error: `title validation: Content.title is ${title.length} chars but ${operation} accepts \u2264${limits.titleMax}`
21215
- };
21216
22340
  }
21217
- return {};
21218
- }
21219
- function validateDigest(content, operation) {
21220
- const max = MODE_LIMITS[operation].digestMax;
21221
- if (max === undefined)
21222
- return {};
21223
- const digest = deriveDigest(content);
21224
- if (digest !== undefined && digest.length > max) {
21225
- return { error: `digest validation: the \u6458\u8981 is ${digest.length} chars but ${operation} accepts \u2264${max}` };
22341
+ const collection = str("collection");
22342
+ if (collection !== "") {
22343
+ const name = "\u5408\u96C6";
22344
+ await step(name, async () => {
22345
+ await openRow(ROW_COLLECTION, "\u5408\u96C6");
22346
+ await waitForVisible(page, TAG_INPUT, 8000);
22347
+ await fillSelector(page, TAG_INPUT, collection);
22348
+ await page.waitForTimeout?.(COLLECTION_SETTLE_MS);
22349
+ await clickSelector(page, `${DIALOG} :text("${collection}")`, 8000);
22350
+ await clickSelector(page, DIALOG_CONFIRM, 8000);
22351
+ });
22352
+ await residual(name);
21226
22353
  }
21227
- return digest === undefined ? {} : { digest };
21228
- }
21229
- function validateAuthor(content, operation) {
21230
- const max = MODE_LIMITS[operation].authorMax;
21231
- const author = content.metadata?.author;
21232
- if (typeof author !== "string" || author.length === 0)
21233
- return {};
21234
- if (max !== undefined && author.length > max) {
21235
- return {
21236
- error: `author validation: Content.metadata.author is ${author.length} chars but ${operation} accepts \u2264${max}`
21237
- };
22354
+ const originalUrl = str("originalUrl");
22355
+ if (originalUrl !== "") {
22356
+ const name = "\u539F\u6587\u94FE\u63A5";
22357
+ const urlRow = await tryFindSelector(page, ROW_URL, 2000);
22358
+ if (urlRow === undefined) {
22359
+ warnings.push(`${name}: \u884C\u4E0D\u5B58\u5728\uFF08\u5F53\u524D\u516C\u4F17\u53F7\u4E0D\u652F\u6301\uFF09\uFF0C\u8BF7\u624B\u52A8\u5904\u7406\u6216\u79FB\u9664 originalUrl`);
22360
+ } else {
22361
+ await step(name, async () => {
22362
+ await openRow(ROW_URL, "\u539F\u6587\u94FE\u63A5");
22363
+ const field = await trySelectors(page, URL_INPUT);
22364
+ if (!field.found)
22365
+ throw new Error("no URL field in the \u539F\u6587\u94FE\u63A5 dialog");
22366
+ await fillSelector(page, field.selector ?? URL_INPUT.join(", "), originalUrl);
22367
+ await clickSelector(page, DIALOG_CONFIRM, 8000);
22368
+ });
22369
+ await residual(name);
22370
+ const locator = page.locator(URL_INPUT[0] ?? "").first();
22371
+ const committed = typeof locator.inputValue === "function" ? await locator.inputValue().catch(() => null) : null;
22372
+ if (norm(committed) !== norm(originalUrl))
22373
+ warnings.push(`${name}: \u5DF2\u63D0\u4EA4\u4F46\u672A\u8BFB\u53D6\u5230\u5DF2\u4FDD\u5B58\u7684\u94FE\u63A5\uFF0C\u8BF7\u6838\u5BF9`);
22374
+ }
21238
22375
  }
21239
- return { author };
21240
- }
21241
- async function validateModeInput(operation, content) {
21242
- const status = validatePublishStatus(content);
21243
- if (status.error)
21244
- return { error: status.error };
21245
- const title = validateTitle(content, operation);
21246
- if (title.error)
21247
- return { error: title.error };
21248
- const author = validateAuthor(content, operation);
21249
- if (author.error)
21250
- return { error: author.error };
21251
- switch (operation) {
21252
- case "\u6587\u7AE0": {
21253
- if ((content.body ?? "").trim().length === 0) {
21254
- return { error: "body validation: Content.body is required for \u6587\u7AE0" };
21255
- }
21256
- const digest = validateDigest(content, operation);
21257
- if (digest.error)
21258
- return { error: digest.error };
21259
- const images = await resolveBodyImages(content);
21260
- if (images.error)
21261
- return { error: images.error };
21262
- if ((images.paths ?? []).length === 0) {
21263
- return {
21264
- error: "cover validation: \u6587\u7AE0 takes its cover from a body image (\u9ED8\u8BA4\u9996\u56FE\u4E3A\u5C01\u9762) but none was supplied \u2014 set metadata.wechatmp.coverPath or metadata.images[]"
21265
- };
22376
+ const claimSource = str("claimSource");
22377
+ if (claimSource !== "") {
22378
+ const name = "\u521B\u4F5C\u6765\u6E90";
22379
+ const claimMatched = async () => {
22380
+ try {
22381
+ const raw = await page.evaluate(CLAIM_COMMITTED);
22382
+ if (raw === null || typeof raw !== "object")
22383
+ return null;
22384
+ return raw.enabled === true && norm(raw.text) === norm(claimSource);
22385
+ } catch {
22386
+ return null;
21266
22387
  }
21267
- return {
21268
- input: {
21269
- operation,
21270
- images: images.paths ?? [],
21271
- ...digest.digest === undefined ? {} : { digest: digest.digest }
22388
+ };
22389
+ if (await claimMatched() !== true) {
22390
+ await step(name, async () => {
22391
+ await openRow(ROW_CLAIM, "\u521B\u4F5C\u6765\u6E90");
22392
+ const dialog = await waitForVisible(page, DIALOG, 3000, { required: false });
22393
+ if (dialog !== undefined) {
22394
+ await clickSelector(page, `${DIALOG} :text("${claimSource}")`, 8000);
22395
+ await clickSelector(page, DIALOG_CONFIRM, 8000);
21272
22396
  }
21273
- };
21274
- }
21275
- case "\u8D34\u56FE": {
21276
- const images = await resolveBodyImages(content);
21277
- if (images.error)
21278
- return { error: images.error };
21279
- if ((images.paths ?? []).length === 0) {
21280
- return {
21281
- error: "image validation: \u8D34\u56FE requires at least one image (metadata.images[] or metadata.wechatmp.coverPath)"
21282
- };
21283
- }
21284
- return { input: { operation, images: images.paths ?? [] } };
21285
- }
21286
- case "\u89C6\u9891": {
21287
- const video = await resolveMediaPath(content, {
21288
- key: "video",
21289
- label: "video validation",
21290
- extensions: VIDEO_EXTENSIONS,
21291
- list: VIDEO_LIST
21292
22397
  });
21293
- if (video.error)
21294
- return { error: video.error };
21295
- return { input: { operation, video: video.path ?? "" } };
21296
22398
  }
21297
- case "\u64AD\u5BA2": {
21298
- const audio = await resolveMediaPath(content, {
21299
- key: "audio",
21300
- label: "audio validation",
21301
- extensions: AUDIO_EXTENSIONS,
21302
- list: AUDIO_LIST
21303
- });
21304
- if (audio.error)
21305
- return { error: audio.error };
21306
- const cover = await resolveMediaPath(content, {
21307
- key: "coverImage",
21308
- label: "cover validation",
21309
- extensions: IMAGE_EXTENSIONS,
21310
- list: IMAGE_LIST
21311
- });
21312
- if (cover.error)
21313
- return { error: cover.error };
21314
- return { input: { operation, audio: audio.path ?? "", cover: cover.path ?? "" } };
22399
+ if (await claimMatched() !== true && !await residual(name)) {
22400
+ warnings.push(`${name}: \u672A\u751F\u6548\uFF0C\u8BF7\u624B\u52A8\u8BBE\u7F6E`);
21315
22401
  }
21316
22402
  }
22403
+ await sweep();
22404
+ return warnings;
21317
22405
  }
21318
22406
 
21319
22407
  // ../../plugins/publishings/wechatmp-pub/src/flow.ts
@@ -21981,7 +23069,86 @@ async function openDialogInventory(page) {
21981
23069
  const raw = await page.evaluate(`[...document.querySelectorAll('.weui-desktop-dialog, .weui-desktop-dialog__wrp')]` + `.filter((d) => d.offsetWidth).map((d) => (d.className || '') + ' :: ' + ` + `(d.innerText || '').replace(/\\s+/g, ' ').slice(0, 160)).join(' || ')`).catch(() => "");
21982
23070
  return typeof raw === "string" ? raw.slice(0, 600) : "";
21983
23071
  }
23072
+ async function runCgiDraft(input, deps) {
23073
+ if (input.operation !== "\u6587\u7AE0")
23074
+ return;
23075
+ try {
23076
+ assertEligibleCgiDraft(deps.content);
23077
+ } catch (error) {
23078
+ if (error instanceof WechatCgiDraftError && error.code === "ineligible")
23079
+ return;
23080
+ throw error;
23081
+ }
23082
+ const metadata = deps.content.metadata ?? {};
23083
+ const bodyImages = input.images;
23084
+ if (bodyImages.length === 0)
23085
+ return;
23086
+ const cover = typeof metadata.coverPath === "string" && metadata.coverPath.length > 0 ? metadata.coverPath : bodyImages[0] ?? "";
23087
+ try {
23088
+ await readLocalImageBytes(cover);
23089
+ } catch {
23090
+ return;
23091
+ }
23092
+ const localImages = {};
23093
+ for (const path of bodyImages)
23094
+ localImages[path] = path;
23095
+ const imageBlock = bodyImages.map((path) => `<img src="${path}">`).join("");
23096
+ try {
23097
+ const session = await extractWechatCgiSession(deps.entry);
23098
+ let media;
23099
+ try {
23100
+ media = await prepareArticleMedia(deps.entry, session, {
23101
+ html: imageBlock + markdownToWechatHtml(bodyOf(deps.content)),
23102
+ coverPath: cover,
23103
+ localImages
23104
+ });
23105
+ } catch (error) {
23106
+ if (error instanceof WechatCgiDraftError)
23107
+ throw error;
23108
+ return ResultSchema.parse({
23109
+ ok: false,
23110
+ target: TARGET,
23111
+ error: error instanceof Error ? error.message : String(error),
23112
+ metadata: { operation: input.operation, publishStatus: deps.publishStatus, stage: "media-preparation" }
23113
+ });
23114
+ }
23115
+ const draft = await createWechatArticleDraft(deps.entry, session, deps.content, media);
23116
+ return ResultSchema.parse({
23117
+ ok: true,
23118
+ target: TARGET,
23119
+ id: draft.appMsgId,
23120
+ url: `https://mp.weixin.qq.com${draft.editPath}`,
23121
+ metadata: {
23122
+ operation: input.operation,
23123
+ publishStatus: deps.publishStatus,
23124
+ stage: "draft-created",
23125
+ appMsgId: draft.appMsgId,
23126
+ coverFileIds: [media.cover.wide.fileId, media.cover.square.fileId],
23127
+ cdnMigratedImages: media.images.length
23128
+ }
23129
+ });
23130
+ } catch (error) {
23131
+ if (!(error instanceof WechatCgiDraftError))
23132
+ throw error;
23133
+ return ResultSchema.parse({
23134
+ ok: false,
23135
+ target: TARGET,
23136
+ error: error.message,
23137
+ metadata: {
23138
+ operation: input.operation,
23139
+ publishStatus: deps.publishStatus,
23140
+ stage: error.stage,
23141
+ ...error.metadata.ambiguousSubmission === true ? { ambiguousSubmission: true } : {}
23142
+ }
23143
+ });
23144
+ }
23145
+ }
21984
23146
  async function runMode(input, deps) {
23147
+ if (input.operation === "\u6587\u7AE0" && deps.publishStatus === "draft") {
23148
+ const cgi = await runCgiDraft(input, deps);
23149
+ if (cgi !== undefined)
23150
+ return cgi;
23151
+ }
21985
23152
  const entry = await enterMode(deps, input.operation);
21986
23153
  const composed = await compose(input, deps, entry);
21987
23154
  const diagnostics = [...composed.warnings, ...composed.enrichWarnings];