publishport-opencli 1.0.9 → 1.0.10

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 (42) hide show
  1. package/clis/_shared/file-inject.js +41 -0
  2. package/clis/_shared/video-publish.js +4 -4
  3. package/clis/bilibili/upload.js +1 -1
  4. package/clis/chatgpt/utils.js +5 -5
  5. package/clis/claude/utils.js +1 -1
  6. package/clis/deepseek/utils.js +7 -7
  7. package/clis/douyin/draft.js +6 -6
  8. package/clis/douyin/publish-image.js +10 -31
  9. package/clis/facebook/post.js +9 -36
  10. package/clis/instagram/post.js +19 -66
  11. package/clis/instagram/reel.js +11 -11
  12. package/clis/threads/post.js +15 -34
  13. package/clis/twitter/post.js +15 -42
  14. package/clis/twitter/utils.js +4 -36
  15. package/clis/wechat-channels/publish.js +29 -57
  16. package/clis/weibo/publish.js +8 -8
  17. package/clis/weixin/create-draft.js +7 -7
  18. package/clis/xianyu/publish.js +2 -2
  19. package/clis/xiaohongshu/publish-video.js +9 -27
  20. package/clis/xiaohongshu/publish.js +32 -72
  21. package/dist/src/browser/bridge-readiness.d.ts +1 -1
  22. package/dist/src/browser/daemon-client.d.ts +72 -1
  23. package/dist/src/browser/daemon-client.js +1 -1
  24. package/dist/src/browser/daemon-version.d.ts +1 -1
  25. package/dist/src/browser/errors.d.ts +2 -0
  26. package/dist/src/browser/errors.js +3 -3
  27. package/dist/src/browser/network-interceptor.d.ts +11 -0
  28. package/dist/src/browser/network-interceptor.js +1 -0
  29. package/dist/src/cli.js +24 -24
  30. package/dist/src/commands/daemon.js +1 -1
  31. package/dist/src/daemon-utils.d.ts +1 -0
  32. package/dist/src/daemon-utils.js +1 -1
  33. package/dist/src/daemon.js +1 -1
  34. package/dist/src/doctor.d.ts +1 -1
  35. package/dist/src/doctor.js +7 -7
  36. package/dist/src/errors.d.ts +8 -0
  37. package/dist/src/errors.js +1 -1
  38. package/dist/src/execution.js +2 -2
  39. package/dist/src/session-lease.d.ts +135 -0
  40. package/dist/src/session-lease.js +1 -0
  41. package/dist/src/session-lease.test.d.ts +1 -0
  42. package/package.json +1 -1
@@ -1,13 +1,13 @@
1
- import*as v from"node:fs";import*as X from"node:path";import{cli as V,Strategy as A}from"@jackwener/opencli/registry";import{ArgumentError as j,AuthRequiredError as f,CommandExecutionError as $}from"@jackwener/opencli/errors";import{assertLiteralContent as k}from"../_shared/content-guard.js";import{THREADS_HOME_URL as C}from"./shared.js";const U=new Set([".jpg",".jpeg",".png",".webp",".gif"]),N=30000,J=500,w=1e4,y=250,R=30000,D=1000,H='input[type="file"]',G=`
1
+ import*as D from"node:fs";import*as v from"node:path";import{cli as V,Strategy as H}from"@jackwener/opencli/registry";import{ArgumentError as q,AuthRequiredError as f,CommandExecutionError as G}from"@jackwener/opencli/errors";import{assertLiteralContent as B}from"../_shared/content-guard.js";import{setInputFiles as A}from"../_shared/file-inject.js";import{THREADS_HOME_URL as k}from"./shared.js";const C=new Set([".jpg",".jpeg",".png",".webp",".gif"]),F=30000,J=500,U=1e4,j=250,h=30000,y=1000,R='input[type="file"]',$=`
2
2
  const isVisible = (el) => {
3
3
  if (!(el instanceof HTMLElement)) return false;
4
4
  const style = window.getComputedStyle(el);
5
5
  const rect = el.getBoundingClientRect();
6
6
  return style.display !== 'none' && style.visibility !== 'hidden' && rect.width > 0 && rect.height > 0;
7
7
  };
8
- `;function T(z){if(!z)throw new $("Browser session required for threads post");return z}function c(z){const K=String(z??"").trim();if(!K)return null;const Q=X.resolve(K),Y=X.extname(Q).toLowerCase();if(!U.has(Y))throw new j(`Unsupported image format "${Y}"`,"Supported formats: jpg, jpeg, png, webp, gif");const W=v.statSync(Q,{throwIfNoEntry:!1});if(!W||!W.isFile())throw new j(`Not a valid file: ${Q}`);return Q}export function buildEnsureComposerOpenJs(){return`
8
+ `;function w(z){if(!z)throw new G("Browser session required for threads post");return z}function T(z){const K=String(z??"").trim();if(!K)return null;const Q=v.resolve(K),W=v.extname(Q).toLowerCase();if(!C.has(W))throw new q(`Unsupported image format "${W}"`,"Supported formats: jpg, jpeg, png, webp, gif");const Y=D.statSync(Q,{throwIfNoEntry:!1});if(!Y||!Y.isFile())throw new q(`Not a valid file: ${Q}`);return Q}export function buildEnsureComposerOpenJs(){return`
9
9
  (() => {
10
- ${G}
10
+ ${$}
11
11
  const pathName = (window.location && window.location.pathname) || '';
12
12
  const onLoginRoute = /\\/login\\/?$/.test(pathName) || /\\/accounts\\/login/.test(pathName);
13
13
  const hasLoginField = !!document.querySelector('input[name="username"], input[name="password"], input[autocomplete="username"], input[autocomplete="current-password"]');
@@ -34,15 +34,15 @@ import*as v from"node:fs";import*as X from"node:path";import{cli as V,Strategy a
34
34
  // 没有登录墙、也没找到入口:composer 可能延迟渲染,交给后续 focus 步骤重试。
35
35
  return { ok: true, opened: false };
36
36
  })()
37
- `}async function M(z){const K=await z.evaluate(buildEnsureComposerOpenJs());if(K?.reason==="auth")throw new f("www.threads.com","Threads login required before posting");if(!K?.ok)throw new $("Failed to open Threads composer")}async function O(z){return z.evaluate(`(() => {
38
- ${G}
37
+ `}async function c(z){const K=await z.evaluate(buildEnsureComposerOpenJs());if(K?.reason==="auth")throw new f("www.threads.com","Threads login required before posting");if(!K?.ok)throw new G("Failed to open Threads composer")}async function x(z){return z.evaluate(`(() => {
38
+ ${$}
39
39
  const boxes = Array.from(document.querySelectorAll('[contenteditable="true"], textarea'));
40
40
  const box = boxes.find(isVisible) || boxes[0];
41
41
  if (!box) return { ok: false, message: 'Could not find the Threads composer text box. Are you logged in?' };
42
42
  box.focus();
43
43
  return { ok: true };
44
44
  })()`)}export function buildVerifyComposerTextJs(z,K,Q){return`(async () => {
45
- ${G}
45
+ ${$}
46
46
  const expected = ${JSON.stringify(z)};
47
47
  const normalize = (s) => String(s || '').replace(/\\u00a0/g, ' ').replace(/\\s+/g, ' ').trim();
48
48
  const normalizedExpected = normalize(expected);
@@ -60,9 +60,9 @@ import*as v from"node:fs";import*as X from"node:path";import{cli as V,Strategy a
60
60
  message: 'Could not verify Threads post text in the composer after typing.',
61
61
  actualText: box ? (box.value || box.innerText || box.textContent || '') : '',
62
62
  };
63
- })()`}async function x(z,K){const Q=Math.ceil(w/y);return z.evaluate(buildVerifyComposerTextJs(K,Q,y))}function h(z){const K=(z instanceof Error?z.message:String(z)).toLowerCase();return K.includes("unknown action")||K.includes("not supported")||K.includes("inserttext returned no inserted flag")}async function d(z,K){const Q=await O(z);if(!Q?.ok)return Q;const Y=[z.nativeType?.bind(z),z.insertText?.bind(z)].filter(Boolean);for(const W of Y)try{await W(K);const Z=await x(z,K);if(Z?.ok)return Z}catch(Z){if(!h(Z))throw Z}return z.evaluate(`(async () => {
63
+ })()`}async function M(z,K){const Q=Math.ceil(U/j);return z.evaluate(buildVerifyComposerTextJs(K,Q,j))}function O(z){const K=(z instanceof Error?z.message:String(z)).toLowerCase();return K.includes("unknown action")||K.includes("not supported")||K.includes("inserttext returned no inserted flag")}async function b(z,K){const Q=await x(z);if(!Q?.ok)return Q;const W=[z.nativeType?.bind(z),z.insertText?.bind(z)].filter(Boolean);for(const Y of W)try{await Y(K);const Z=await M(z,K);if(Z?.ok)return Z}catch(Z){if(!O(Z))throw Z}return z.evaluate(`(async () => {
64
64
  try {
65
- ${G}
65
+ ${$}
66
66
  const boxes = Array.from(document.querySelectorAll('[contenteditable="true"], textarea'));
67
67
  const box = boxes.find(isVisible) || boxes[0];
68
68
  if (!box) return { ok: false, message: 'Could not find the Threads composer text box. Are you logged in?' };
@@ -85,35 +85,16 @@ import*as v from"node:fs";import*as X from"node:path";import{cli as V,Strategy a
85
85
  return { ok: false, message: 'Could not verify Threads post text in the composer after typing.', actualText: actual };
86
86
  } catch (e) { return { ok: false, message: String(e) }; }
87
87
  })()`)}export function buildUploadProbeJs(z,K){return`(async () => {
88
- ${G}
88
+ ${$}
89
89
  for (let i = 0; i < ${JSON.stringify(z)}; i++) {
90
90
  await new Promise((r) => setTimeout(r, ${JSON.stringify(K)}));
91
91
  const previewCount = document.querySelectorAll('img[src^="blob:"], video[src^="blob:"], [data-testid*="attachment"] img, [role="dialog"] img[alt]').length;
92
92
  if (previewCount > 0) return { ok: true, previewCount };
93
93
  }
94
- return { ok: false, message: 'Threads image upload timed out (${N/1000}s).' };
95
- })()`}async function B(z,K){const Q=X.extname(K).toLowerCase(),Y=Q===".png"?"image/png":Q===".gif"?"image/gif":Q===".webp"?"image/webp":"image/jpeg",W={name:X.basename(K),mime:Y,base64:v.readFileSync(K).toString("base64")},Z=await z.evaluate(`(() => {
96
- const input = document.querySelector(${JSON.stringify(H)});
97
- if (!input) return { ok: false, error: 'No file input found' };
98
- const file = ${JSON.stringify(W)};
99
- const dt = new DataTransfer();
100
- const bin = atob(file.base64);
101
- const bytes = new Uint8Array(bin.length);
102
- for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
103
- dt.items.add(new File([bytes], file.name, { type: file.mime }));
104
- try {
105
- Object.defineProperty(input, 'files', { value: dt.files, writable: false, configurable: true });
106
- } catch (e) {
107
- const nativeSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'files');
108
- if (nativeSetter && nativeSetter.set) nativeSetter.set.call(input, dt.files);
109
- }
110
- if (!input.files || input.files.length < 1) return { ok: false, error: 'Could not assign file to input' };
111
- input.dispatchEvent(new Event('input', { bubbles: true }));
112
- input.dispatchEvent(new Event('change', { bubbles: true }));
113
- return { ok: true };
114
- })()`);if(!Z?.ok)throw new $(`Threads image upload failed: ${Z?.error??"unknown error"}`)}async function I(z,K){if(z.setFileInput)try{await z.setFileInput([K],H)}catch(W){const Z=W instanceof Error?W.message:String(W);if(!(Z.includes("Unknown action")||Z.includes("set-file-input")||Z.includes("not supported")||Z.includes("Not allowed")))throw W;await B(z,K)}else await B(z,K);const Q=Math.ceil(N/J),Y=await z.evaluate(buildUploadProbeJs(Q,J));if(!Y?.ok)throw new $(Y?.message??`Threads image upload timed out (${N/1000}s).`)}export function buildSubmitJs(){return`
94
+ return { ok: false, message: 'Threads image upload timed out (${F/1000}s).' };
95
+ })()`}async function d(z,K){await A(z,R,[K]);const Q=Math.ceil(F/J),W=await z.evaluate(buildUploadProbeJs(Q,J));if(!W?.ok)throw new G(W?.message??`Threads image upload timed out (${F/1000}s).`)}export function buildSubmitJs(){return`
115
96
  (() => {
116
- ${G}
97
+ ${$}
117
98
  const boxes = Array.from(document.querySelectorAll('[contenteditable="true"], textarea'));
118
99
  const box = boxes.find(isVisible) || boxes[0];
119
100
  const container = (box && (box.closest('[role="dialog"]') || box.closest('form'))) || document.body;
@@ -136,9 +117,9 @@ import*as v from"node:fs";import*as X from"node:path";import{cli as V,Strategy a
136
117
  target.click();
137
118
  return { ok: true, label: labelOf(target) };
138
119
  })()
139
- `}async function L(z){return z.evaluate(buildSubmitJs())}export function buildPublishStatusProbeJs(){return`
120
+ `}async function I(z){return z.evaluate(buildSubmitJs())}export function buildPublishStatusProbeJs(){return`
140
121
  (() => {
141
- ${G}
122
+ ${$}
142
123
  const box = Array.from(document.querySelectorAll('[contenteditable="true"], textarea')).find(isVisible);
143
124
  const dialogOpen = Array.from(document.querySelectorAll('[role="dialog"]')).some(isVisible);
144
125
  const alertText = Array.from(document.querySelectorAll('[role="alert"]'))
@@ -149,4 +130,4 @@ import*as v from"node:fs";import*as X from"node:path";import{cli as V,Strategy a
149
130
  const done = !box && !dialogOpen;
150
131
  return { ok: done && !failed, failed, settled: done };
151
132
  })()
152
- `}async function S(z){const K=Math.ceil(R/D);let Q=0;for(let Y=0;Y<K;Y++){const W=await z.evaluate(buildPublishStatusProbeJs());if(W?.failed)return{ok:!1,message:"Threads post could not be published (share failed)."};if(W?.ok)return{ok:!0,message:"Threads post published successfully."};if(W?.settled){Q+=1;if(Q>=3)return{ok:!0,message:"Threads post published successfully."}}else Q=0;if(Y<K-1)await z.wait(D/1000)}return{ok:!1,message:"Threads post was not confirmed sent; verify via readback before retrying."}}V({site:"threads",name:"post",access:"write",description:"Post a new thread (text, optional single image) on Threads",domain:"www.threads.com",strategy:A.UI,browser:!0,args:[{name:"text",type:"string",required:!0,positional:!0,help:"The text content of the thread"},{name:"image",type:"string",required:!1,valueRequired:!0,help:"Optional single image path (jpg/jpeg/png/webp/gif)"}],columns:["status","message","text","url"],func:async(z,K)=>{const Q=T(z),Y=K.image?c(K.image):null,W=String(K.text??"");k(W,{label:"串文正文"});if(!W.trim()&&!Y)throw new j("Thread text is required.","Provide the post text as the positional argument");await Q.goto(C);await Q.wait(2);await M(Q);await Q.wait(1);if(Y)await I(Q,Y);const Z=await d(Q,W);if(!Z?.ok)throw new $(Z?.message??"Could not type Threads post text.");await Q.wait(1);const q=await L(Q);if(!q?.ok)throw new $(q?.message??"Threads post button not found.");const F=await S(Q);if(!F?.ok)throw new $(F?.message??"Threads post was not confirmed sent; verify via readback before retrying.");return[{status:"success",message:F.message,text:W,url:""}]}});
133
+ `}async function L(z){const K=Math.ceil(h/y);let Q=0;for(let W=0;W<K;W++){const Y=await z.evaluate(buildPublishStatusProbeJs());if(Y?.failed)return{ok:!1,message:"Threads post could not be published (share failed)."};if(Y?.ok)return{ok:!0,message:"Threads post published successfully."};if(Y?.settled){Q+=1;if(Q>=3)return{ok:!0,message:"Threads post published successfully."}}else Q=0;if(W<K-1)await z.wait(y/1000)}return{ok:!1,message:"Threads post was not confirmed sent; verify via readback before retrying."}}V({site:"threads",name:"post",access:"write",description:"Post a new thread (text, optional single image) on Threads",domain:"www.threads.com",strategy:H.UI,browser:!0,args:[{name:"text",type:"string",required:!0,positional:!0,help:"The text content of the thread"},{name:"image",type:"string",required:!1,valueRequired:!0,help:"Optional single image path (jpg/jpeg/png/webp/gif)"}],columns:["status","message","text","url"],func:async(z,K)=>{const Q=w(z),W=K.image?T(K.image):null,Y=String(K.text??"");B(Y,{label:"串文正文"});if(!Y.trim()&&!W)throw new q("Thread text is required.","Provide the post text as the positional argument");await Q.goto(k);await Q.wait(2);await c(Q);await Q.wait(1);if(W)await d(Q,W);const Z=await b(Q,Y);if(!Z?.ok)throw new G(Z?.message??"Could not type Threads post text.");await Q.wait(1);const N=await I(Q);if(!N?.ok)throw new G(N?.message??"Threads post button not found.");const X=await L(Q);if(!X?.ok)throw new G(X?.message??"Threads post was not confirmed sent; verify via readback before retrying.");return[{status:"success",message:X.message,text:Y,url:""}]}});
@@ -1,11 +1,11 @@
1
- import*as B from"node:fs";import*as j from"node:path";import{cli as U,Strategy as w}from"@jackwener/opencli/registry";import{AuthRequiredError as N,CommandExecutionError as G}from"@jackwener/opencli/errors";import{describeTwitterApiError as L,resolveTwitterOperationMetadata as I,unwrapBrowserResult as M}from"./shared.js";import{isRecoverableFileInputError as S,TWITTER_BEARER_TOKEN as O}from"./utils.js";import{assertLiteralContent as m}from"../_shared/content-guard.js";const f=4,y=500,v=30000,A=250,h=1e4,J=500,k=15000,x="https://x.com/compose/post",_="https://x.com/home",D='input[type="file"][data-testid="fileInput"]',c=new Set([".jpg",".jpeg",".png",".gif",".webp"]),P={queryId:"5CdvsV_zjv4L64XFifAglw",features:{premium_content_api_read_enabled:!1,communities_web_enable_tweet_community_results_fetch:!0,c9s_tweet_anatomy_moderator_badge_enabled:!0,responsive_web_grok_analyze_button_fetch_trends_enabled:!1,responsive_web_grok_analyze_post_followups_enabled:!0,rweb_cashtags_composer_attachment_enabled:!0,responsive_web_jetfuel_frame:!0,responsive_web_grok_share_attachment_enabled:!0,responsive_web_grok_annotations_enabled:!0,responsive_web_edit_tweet_api_enabled:!0,rweb_conversational_replies_downvote_enabled:!1,graphql_is_translatable_rweb_tweet_is_translatable_enabled:!0,view_counts_everywhere_api_enabled:!0,longform_notetweets_consumption_enabled:!0,responsive_web_twitter_article_tweet_consumption_enabled:!0,content_disclosure_indicator_enabled:!0,content_disclosure_ai_generated_indicator_enabled:!0,responsive_web_grok_show_grok_translated_post:!0,responsive_web_grok_analysis_button_from_backend:!0,post_ctas_fetch_enabled:!0,longform_notetweets_rich_text_read_enabled:!0,longform_notetweets_inline_media_enabled:!1,profile_label_improvements_pcf_label_in_post_enabled:!0,responsive_web_profile_redirect_enabled:!1,rweb_tipjar_consumption_enabled:!1,verified_phone_label_enabled:!1,articles_preview_enabled:!0,rweb_cashtags_enabled:!0,responsive_web_grok_community_note_auto_translation_is_enabled:!0,responsive_web_graphql_skip_user_profile_image_extensions_enabled:!1,freedom_of_speech_not_reach_fetch_enabled:!0,standardized_nudges_misinfo:!0,tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled:!0,responsive_web_grok_image_annotation_enabled:!0,responsive_web_grok_imagine_annotation_enabled:!0,responsive_web_graphql_timeline_navigation_enabled:!0},fieldToggles:{}};function b(z){const Q=z.split(",").map((H)=>H.trim()).filter(Boolean);if(Q.length>f)throw new G(`Too many images: ${Q.length} (max ${f})`);return Q.map((H)=>{const Y=j.resolve(H),$=j.extname(Y).toLowerCase();if(!c.has($))throw new G(`Unsupported image format "${$}". Supported: jpg, png, gif, webp`);const Z=B.statSync(Y,{throwIfNoEntry:!1});if(!Z||!Z.isFile())throw new G(`Not a valid file: ${Y}`);return Y})}function T(z){const H=(z instanceof Error?z.message:String(z)).toLowerCase();return H.includes("unknown action")||H.includes("not supported")||H.includes("inserttext returned no inserted flag")}async function d(z){return z.evaluate(`(() => {
1
+ import*as R from"node:fs";import*as J from"node:path";import{cli as h,Strategy as C}from"@jackwener/opencli/registry";import{AuthRequiredError as j,CommandExecutionError as q}from"@jackwener/opencli/errors";import{describeTwitterApiError as U,resolveTwitterOperationMetadata as w,unwrapBrowserResult as I}from"./shared.js";import{TWITTER_BEARER_TOKEN as L}from"./utils.js";import{assertLiteralContent as P}from"../_shared/content-guard.js";import{setInputFiles as b}from"../_shared/file-inject.js";const B=4,N=500,D=30000,f=250,m=1e4,v=500,y=15000,S="https://x.com/compose/post",M="https://x.com/home",A='input[type="file"][data-testid="fileInput"]',O=new Set([".jpg",".jpeg",".png",".gif",".webp"]),x={queryId:"5CdvsV_zjv4L64XFifAglw",features:{premium_content_api_read_enabled:!1,communities_web_enable_tweet_community_results_fetch:!0,c9s_tweet_anatomy_moderator_badge_enabled:!0,responsive_web_grok_analyze_button_fetch_trends_enabled:!1,responsive_web_grok_analyze_post_followups_enabled:!0,rweb_cashtags_composer_attachment_enabled:!0,responsive_web_jetfuel_frame:!0,responsive_web_grok_share_attachment_enabled:!0,responsive_web_grok_annotations_enabled:!0,responsive_web_edit_tweet_api_enabled:!0,rweb_conversational_replies_downvote_enabled:!1,graphql_is_translatable_rweb_tweet_is_translatable_enabled:!0,view_counts_everywhere_api_enabled:!0,longform_notetweets_consumption_enabled:!0,responsive_web_twitter_article_tweet_consumption_enabled:!0,content_disclosure_indicator_enabled:!0,content_disclosure_ai_generated_indicator_enabled:!0,responsive_web_grok_show_grok_translated_post:!0,responsive_web_grok_analysis_button_from_backend:!0,post_ctas_fetch_enabled:!0,longform_notetweets_rich_text_read_enabled:!0,longform_notetweets_inline_media_enabled:!1,profile_label_improvements_pcf_label_in_post_enabled:!0,responsive_web_profile_redirect_enabled:!1,rweb_tipjar_consumption_enabled:!1,verified_phone_label_enabled:!1,articles_preview_enabled:!0,rweb_cashtags_enabled:!0,responsive_web_grok_community_note_auto_translation_is_enabled:!0,responsive_web_graphql_skip_user_profile_image_extensions_enabled:!1,freedom_of_speech_not_reach_fetch_enabled:!0,standardized_nudges_misinfo:!0,tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled:!0,responsive_web_grok_image_annotation_enabled:!0,responsive_web_grok_imagine_annotation_enabled:!0,responsive_web_graphql_timeline_navigation_enabled:!0},fieldToggles:{}};function c(z){const Q=z.split(",").map((H)=>H.trim()).filter(Boolean);if(Q.length>B)throw new q(`Too many images: ${Q.length} (max ${B})`);return Q.map((H)=>{const Y=J.resolve(H),$=J.extname(Y).toLowerCase();if(!O.has($))throw new q(`Unsupported image format "${$}". Supported: jpg, png, gif, webp`);const Z=R.statSync(Y,{throwIfNoEntry:!1});if(!Z||!Z.isFile())throw new q(`Not a valid file: ${Y}`);return Y})}function T(z){const H=(z instanceof Error?z.message:String(z)).toLowerCase();return H.includes("unknown action")||H.includes("not supported")||H.includes("inserttext returned no inserted flag")}async function _(z){return z.evaluate(`(() => {
2
2
  const visible = (el) => !!el && (el.offsetParent !== null || el.getClientRects().length > 0);
3
3
  const boxes = Array.from(document.querySelectorAll('[data-testid="tweetTextarea_0"]'));
4
4
  const box = boxes.find(visible) || boxes[0];
5
5
  if (!box) return { ok: false, message: 'Could not find the tweet composer text area. Are you logged in?' };
6
6
  box.focus();
7
7
  return { ok: true };
8
- })()`)}async function l(z,Q){const H=Math.ceil(h/A);return z.evaluate(`(async () => {
8
+ })()`)}async function d(z,Q){const H=Math.ceil(m/f);return z.evaluate(`(async () => {
9
9
  const expected = ${JSON.stringify(Q)};
10
10
  const normalize = s => String(s || '').replace(/ /g, ' ').replace(/s+/g, ' ').trim();
11
11
  const normalizedExpected = normalize(expected);
@@ -13,7 +13,7 @@ import*as B from"node:fs";import*as j from"node:path";import{cli as U,Strategy a
13
13
  const box = document.querySelector('[data-testid="tweetTextarea_0"]');
14
14
  const actual = box ? (box.innerText || box.textContent || '') : '';
15
15
  if (box && normalize(actual).includes(normalizedExpected)) return { ok: true };
16
- await new Promise(r => setTimeout(r, ${JSON.stringify(A)}));
16
+ await new Promise(r => setTimeout(r, ${JSON.stringify(f)}));
17
17
  }
18
18
  const box = document.querySelector('[data-testid="tweetTextarea_0"]');
19
19
  return {
@@ -21,7 +21,7 @@ import*as B from"node:fs";import*as j from"node:path";import{cli as U,Strategy a
21
21
  message: 'Could not verify tweet text in the composer after typing.',
22
22
  actualText: box ? (box.innerText || box.textContent || '') : ''
23
23
  };
24
- })()`)}async function n(z,Q){const H=await d(z);if(!H?.ok)return H;const Y=[z.nativeType?.bind(z),z.insertText?.bind(z)].filter(Boolean);for(const $ of Y)try{await $(Q);const Z=await l(z,Q);if(Z?.ok)return Z}catch(Z){if(!T(Z))throw Z}return z.evaluate(`(async () => {
24
+ })()`)}async function l(z,Q){const H=await _(z);if(!H?.ok)return H;const Y=[z.nativeType?.bind(z),z.insertText?.bind(z)].filter(Boolean);for(const $ of Y)try{await $(Q);const Z=await d(z,Q);if(Z?.ok)return Z}catch(Z){if(!T(Z))throw Z}return z.evaluate(`(async () => {
25
25
  try {
26
26
  const visible = (el) => !!el && (el.offsetParent !== null || el.getClientRects().length > 0);
27
27
  const boxes = Array.from(document.querySelectorAll('[data-testid="tweetTextarea_0"]'));
@@ -40,11 +40,11 @@ import*as B from"node:fs";import*as j from"node:path";import{cli as U,Strategy a
40
40
  if (normalize(actual).includes(normalize(textToInsert))) return { ok: true };
41
41
  return { ok: false, message: 'Could not verify tweet text in the composer after typing.', actualText: actual };
42
42
  } catch (e) { return { ok: false, message: String(e) }; }
43
- })()`)}async function E(z,Q){const H=Math.ceil(v/y);return z.evaluate(`(async () => {
43
+ })()`)}async function n(z,Q){const H=Math.ceil(D/N);return z.evaluate(`(async () => {
44
44
  const expected = ${JSON.stringify(Q)};
45
45
  const visible = (el) => !!el && (el.offsetParent !== null || el.getClientRects().length > 0);
46
46
  for (let i = 0; i < ${JSON.stringify(H)}; i++) {
47
- await new Promise(r => setTimeout(r, ${JSON.stringify(y)}));
47
+ await new Promise(r => setTimeout(r, ${JSON.stringify(N)}));
48
48
  const attachments = document.querySelector('[data-testid="attachments"]');
49
49
  const previewCount = Math.max(
50
50
  attachments ? attachments.querySelectorAll('[role="group"], img, video').length : 0,
@@ -58,35 +58,8 @@ import*as B from"node:fs";import*as j from"node:path";import{cli as U,Strategy a
58
58
  const buttonReady = !!button && !button.disabled && button.getAttribute('aria-disabled') !== 'true';
59
59
  if (previewCount >= expected && buttonReady) return { ok: true, previewCount };
60
60
  }
61
- return { ok: false, message: 'Image upload timed out (${v/1000}s).' };
62
- })()`)}async function R(z,Q){const H=Q.map(($)=>{const Z=j.extname($).toLowerCase(),q=Z===".png"?"image/png":Z===".gif"?"image/gif":Z===".webp"?"image/webp":"image/jpeg";return{name:j.basename($),mime:q,base64:B.readFileSync($).toString("base64")}}),Y=await z.evaluate(`(() => {
63
- const input = document.querySelector(${JSON.stringify(D)});
64
- if (!input) return { ok: false, error: 'No file input found' };
65
- const dt = new DataTransfer();
66
- for (const file of ${JSON.stringify(H)}) {
67
- const bin = atob(file.base64);
68
- const bytes = new Uint8Array(bin.length);
69
- for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
70
- dt.items.add(new File([bytes], file.name, { type: file.mime }));
71
- }
72
- let assigned = false;
73
- try {
74
- Object.defineProperty(input, 'files', { value: dt.files, writable: false, configurable: true });
75
- assigned = input.files && input.files.length >= ${JSON.stringify(Q.length)};
76
- } catch(e) {
77
- try {
78
- const nativeInputFileSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'files');
79
- if (nativeInputFileSetter && nativeInputFileSetter.set) {
80
- nativeInputFileSetter.set.call(input, dt.files);
81
- assigned = input.files && input.files.length >= ${JSON.stringify(Q.length)};
82
- }
83
- } catch(e2) { /* ignore */ }
84
- }
85
- if (!assigned) return { ok: false, error: 'Could not assign files to input' };
86
- input.dispatchEvent(new Event('change', { bubbles: true }));
87
- input.dispatchEvent(new Event('input', { bubbles: true }));
88
- return { ok: true };
89
- })()`);if(!Y?.ok)throw new G(`Image upload failed (base64 fallback): ${Y?.error??"unknown error"}`)}async function u(z,Q){const H=Math.ceil(k/J),Y=await z.evaluate(`(async () => {
61
+ return { ok: false, message: 'Image upload timed out (${D/1000}s).' };
62
+ })()`)}async function E(z,Q){const H=Math.ceil(y/v),Y=await z.evaluate(`(async () => {
90
63
  try {
91
64
  const visible = (el) => !!el && (el.offsetParent !== null || el.getClientRects().length > 0);
92
65
  // For text-only posts the Tweet button only becomes enabled once
@@ -99,13 +72,13 @@ import*as B from"node:fs";import*as j from"node:path";import{cli as U,Strategy a
99
72
  const buttons = Array.from(document.querySelectorAll('[data-testid="tweetButtonInline"], [data-testid="tweetButton"]'));
100
73
  btn = buttons.find((el) => visible(el) && !el.disabled && el.getAttribute('aria-disabled') !== 'true');
101
74
  if (btn) break;
102
- await new Promise(r => setTimeout(r, ${JSON.stringify(J)}));
75
+ await new Promise(r => setTimeout(r, ${JSON.stringify(v)}));
103
76
  }
104
77
  if (!btn) return { ok: false, message: 'Tweet button is disabled or not found.' };
105
78
  btn.click();
106
79
  return { ok: true };
107
80
  } catch (e) { return { ok: false, message: String(e) }; }
108
- })()`);if(!Y?.ok)return Y;const $=Math.ceil(k/J);return z.evaluate(`(async () => {
81
+ })()`);if(!Y?.ok)return Y;const $=Math.ceil(y/v);return z.evaluate(`(async () => {
109
82
  const expected = ${JSON.stringify(Q)};
110
83
  const normalize = s => String(s || '').replace(/ /g, ' ').replace(/s+/g, ' ').trim();
111
84
  const expectedText = normalize(expected);
@@ -124,7 +97,7 @@ import*as B from"node:fs";import*as j from"node:path";import{cli as U,Strategy a
124
97
  return {};
125
98
  };
126
99
  for (let i = 0; i < ${JSON.stringify($)}; i++) {
127
- await new Promise(r => setTimeout(r, ${JSON.stringify(J)}));
100
+ await new Promise(r => setTimeout(r, ${JSON.stringify(v)}));
128
101
  const toasts = Array.from(document.querySelectorAll('[role="alert"], [data-testid="toast"]'))
129
102
  .filter((el) => visible(el));
130
103
  const successToast = toasts.find((el) => /sent|posted|your post was sent|your tweet was sent/i.test(el.textContent || ''));
@@ -144,10 +117,10 @@ import*as B from"node:fs";import*as j from"node:path";import{cli as U,Strategy a
144
117
  }
145
118
  }
146
119
  return { ok: false, message: 'Tweet submission did not complete before timeout.' };
147
- })()`)}function C(z){const Q=Array.isArray(z?.errors)?z.errors:[];if(Q.length===0)return"";return Q.map((H)=>H?.message||JSON.stringify(H)).filter(Boolean).join("; ").slice(0,300)}function i(z){const Q=z?.data?.create_tweet?.tweet_results?.result,H=Q?.tweet||Q;if(!H||typeof H!=="object")return null;const Y=String(H.rest_id||H.legacy?.id_str||"").trim();if(!/^\d+$/.test(Y))return null;const $=H.core?.user_results?.result,Z=String($?.core?.screen_name||$?.legacy?.screen_name||"").trim();return{id:Y,url:Z?`https://x.com/${Z}/status/${Y}`:`https://x.com/i/status/${Y}`}}async function g(z,Q){await z.goto(_,{waitUntil:"load",settleMs:1000});const Y=(await z.getCookies({url:"https://x.com"})).find((X)=>X.name==="ct0")?.value||null;if(!Y)throw new N("x.com","Not logged into x.com (no ct0 cookie)");const $=await I(z,"CreateTweet",P),Z={variables:{tweet_text:Q,dark_request:!1,media:{media_entities:[],possibly_sensitive:!1},semantic_annotation_ids:[]},features:$.features,queryId:$.queryId};if($.fieldToggles&&Object.keys($.fieldToggles).length>0)Z.fieldToggles=$.fieldToggles;const q=JSON.stringify({Authorization:`Bearer ${decodeURIComponent(O)}`,"X-Csrf-Token":Y,"X-Twitter-Auth-Type":"OAuth2Session","X-Twitter-Active-User":"yes","Content-Type":"application/json"}),W=`/i/api/graphql/${$.queryId}/CreateTweet`,K=JSON.stringify(Z),V=M(await z.evaluate(`async () => {
148
- const r = await fetch(${JSON.stringify(W)}, {
120
+ })()`)}function k(z){const Q=Array.isArray(z?.errors)?z.errors:[];if(Q.length===0)return"";return Q.map((H)=>H?.message||JSON.stringify(H)).filter(Boolean).join("; ").slice(0,300)}function g(z){const Q=z?.data?.create_tweet?.tweet_results?.result,H=Q?.tweet||Q;if(!H||typeof H!=="object")return null;const Y=String(H.rest_id||H.legacy?.id_str||"").trim();if(!/^\d+$/.test(Y))return null;const $=H.core?.user_results?.result,Z=String($?.core?.screen_name||$?.legacy?.screen_name||"").trim();return{id:Y,url:Z?`https://x.com/${Z}/status/${Y}`:`https://x.com/i/status/${Y}`}}async function u(z,Q){await z.goto(M,{waitUntil:"load",settleMs:1000});const Y=(await z.getCookies({url:"https://x.com"})).find((G)=>G.name==="ct0")?.value||null;if(!Y)throw new j("x.com","Not logged into x.com (no ct0 cookie)");const $=await w(z,"CreateTweet",x),Z={variables:{tweet_text:Q,dark_request:!1,media:{media_entities:[],possibly_sensitive:!1},semantic_annotation_ids:[]},features:$.features,queryId:$.queryId};if($.fieldToggles&&Object.keys($.fieldToggles).length>0)Z.fieldToggles=$.fieldToggles;const F=JSON.stringify({Authorization:`Bearer ${decodeURIComponent(L)}`,"X-Csrf-Token":Y,"X-Twitter-Auth-Type":"OAuth2Session","X-Twitter-Active-User":"yes","Content-Type":"application/json"}),V=`/i/api/graphql/${$.queryId}/CreateTweet`,K=JSON.stringify(Z),W=I(await z.evaluate(`async () => {
121
+ const r = await fetch(${JSON.stringify(V)}, {
149
122
  method: 'POST',
150
- headers: ${q},
123
+ headers: ${F},
151
124
  credentials: 'include',
152
125
  body: ${JSON.stringify(K)},
153
126
  });
@@ -155,4 +128,4 @@ import*as B from"node:fs";import*as j from"node:path";import{cli as U,Strategy a
155
128
  let bodyJson = null;
156
129
  try { bodyJson = JSON.parse(bodyText); } catch {}
157
130
  return { ok: r.ok, httpStatus: r.status, bodyJson, bodyText };
158
- }`));if(V?.httpStatus===401||V?.httpStatus===403)throw new N("x.com",`Twitter CreateTweet returned HTTP ${V.httpStatus}`);if(!V?.ok){const X=C(V?.bodyJson);return{ok:!1,message:L("CreateTweet",V?.httpStatus??0,X||void 0)}}const F=i(V.bodyJson);if(!F){const X=C(V.bodyJson);return{ok:!1,message:X?`CreateTweet failed: ${X}`:"CreateTweet returned no created tweet id."}}return{ok:!0,message:"Tweet posted successfully.",...F}}U({site:"twitter",name:"post",access:"write",description:"Post a new tweet/thread",domain:"x.com",strategy:w.UI,browser:!0,args:[{name:"text",type:"string",required:!0,positional:!0,help:"The text content of the tweet"},{name:"images",type:"string",required:!1,help:"Image paths, comma-separated, max 4 (jpg/png/gif/webp)"}],columns:["status","message","text","id","url"],func:async(z,Q)=>{if(!z)throw new G("Browser session required for twitter post");const H=Q.images?b(String(Q.images)):[],Y=String(Q.text??"");m(Y,{label:"推文内容"});try{await z.goto(x,{waitUntil:"load",settleMs:2500});await z.wait({selector:'[data-testid="tweetTextarea_0"]',timeout:15})}catch(W){if(H.length>0)throw W;const K=await g(z,Y);if(!K?.ok)throw new G(K?.message??"Tweet failed to post.");return[{status:"success",message:K?.message??"Tweet posted.",text:Y,...K?.id?{id:K.id}:{},...K?.url?{url:K.url}:{}}]}if(H.length>0){await z.wait({selector:D,timeout:20});if(z.setFileInput)try{await z.setFileInput(H,D)}catch(K){if(!S(K))throw K;await R(z,H)}else await R(z,H);const W=await E(z,H.length);if(!W?.ok)throw new G(W?.message??`Image upload timed out (${v/1000}s).`)}const $=await n(z,Y);if(!$?.ok)throw new G($?.message??"Could not type tweet text.");await z.wait(1);let Z=null,q=null;try{Z=await u(z,Y)}catch(W){q=W}if(!Z?.ok){const{confirmTweetByReadback:W}=await import("./tweets.js"),K=await W(z,Y);if(K.found)return[{status:"success",message:"提交回执未确认(可能超时),但发后回查已在时间线确认发布成功,请勿重发。",text:Y,...K.id?{id:K.id}:{},...K.url?{url:K.url}:{},confirmed_by:"readback"}];const V=q?String(q&&q.message||q):Z?.message??"Tweet was not confirmed sent.",F=K.error?`发后回查失败(${K.error})——无法确认,重试前请先人工核对时间线`:`发后回查已检查最近 ${K.checked} 条推文,未发现该内容,可安全重试`;throw new G(`${V};${F}`)}return[{status:"success",message:Z?.message??"Tweet posted.",text:Y,...Z?.id?{id:Z.id}:{},...Z?.url?{url:Z.url}:{}}]}});
131
+ }`));if(W?.httpStatus===401||W?.httpStatus===403)throw new j("x.com",`Twitter CreateTweet returned HTTP ${W.httpStatus}`);if(!W?.ok){const G=k(W?.bodyJson);return{ok:!1,message:U("CreateTweet",W?.httpStatus??0,G||void 0)}}const X=g(W.bodyJson);if(!X){const G=k(W.bodyJson);return{ok:!1,message:G?`CreateTweet failed: ${G}`:"CreateTweet returned no created tweet id."}}return{ok:!0,message:"Tweet posted successfully.",...X}}h({site:"twitter",name:"post",access:"write",description:"Post a new tweet/thread",domain:"x.com",strategy:C.UI,browser:!0,args:[{name:"text",type:"string",required:!0,positional:!0,help:"The text content of the tweet"},{name:"images",type:"string",required:!1,help:"Image paths, comma-separated, max 4 (jpg/png/gif/webp)"}],columns:["status","message","text","id","url"],func:async(z,Q)=>{if(!z)throw new q("Browser session required for twitter post");const H=Q.images?c(String(Q.images)):[],Y=String(Q.text??"");P(Y,{label:"推文内容"});try{await z.goto(S,{waitUntil:"load",settleMs:2500});await z.wait({selector:'[data-testid="tweetTextarea_0"]',timeout:15})}catch(V){if(H.length>0)throw V;const K=await u(z,Y);if(!K?.ok)throw new q(K?.message??"Tweet failed to post.");return[{status:"success",message:K?.message??"Tweet posted.",text:Y,...K?.id?{id:K.id}:{},...K?.url?{url:K.url}:{}}]}if(H.length>0){await z.wait({selector:A,timeout:20});await b(z,A,H);const V=await n(z,H.length);if(!V?.ok)throw new q(V?.message??`Image upload timed out (${D/1000}s).`)}const $=await l(z,Y);if(!$?.ok)throw new q($?.message??"Could not type tweet text.");await z.wait(1);let Z=null,F=null;try{Z=await E(z,Y)}catch(V){F=V}if(!Z?.ok){const{confirmTweetByReadback:V}=await import("./tweets.js"),K=await V(z,Y);if(K.found)return[{status:"success",message:"提交回执未确认(可能超时),但发后回查已在时间线确认发布成功,请勿重发。",text:Y,...K.id?{id:K.id}:{},...K.url?{url:K.url}:{},confirmed_by:"readback"}];const W=F?String(F&&F.message||F):Z?.message??"Tweet was not confirmed sent.",X=K.error?`发后回查失败(${K.error})——无法确认,重试前请先人工核对时间线`:`发后回查已检查最近 ${K.checked} 条推文,未发现该内容,可安全重试`;throw new q(`${W};${X}`)}return[{status:"success",message:Z?.message??"Tweet posted.",text:Y,...Z?.id?{id:Z.id}:{},...Z?.url?{url:Z.url}:{}}]}});
@@ -1,40 +1,8 @@
1
- import*as k from"node:fs";import*as X from"node:os";import*as F from"node:path";import{ArgumentError as V}from"@jackwener/opencli/errors";export const TWITTER_BEARER_TOKEN="AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA",COMPOSER_FILE_INPUT_SELECTOR='input[type="file"][data-testid="fileInput"]',SUPPORTED_IMAGE_EXTENSIONS=new Set([".jpg",".jpeg",".png",".gif",".webp"]),MAX_IMAGE_SIZE_BYTES=20971520;const B=500,W=30000,Y={"image/jpeg":".jpg","image/jpg":".jpg","image/png":".png","image/gif":".gif","image/webp":".webp"};export function resolveImagePath(q){const J=F.resolve(q);if(!k.existsSync(J))throw new V(`Image file not found: ${J}`);const K=F.extname(J).toLowerCase();if(!SUPPORTED_IMAGE_EXTENSIONS.has(K))throw new V(`Unsupported image format "${K}". Supported: jpg, jpeg, png, gif, webp`);const Q=k.statSync(J);if(Q.size>MAX_IMAGE_SIZE_BYTES)throw new V(`Image too large: ${(Q.size/1024/1024).toFixed(1)} MB (max ${MAX_IMAGE_SIZE_BYTES/1024/1024} MB)`);return J}export function resolveImageExtension(q,J){const K=(J||"").split(";")[0].trim().toLowerCase();if(K&&Y[K])return Y[K];try{const Q=new URL(q).pathname,$=F.extname(Q).toLowerCase();if(SUPPORTED_IMAGE_EXTENSIONS.has($))return $}catch{}throw new V(`Unsupported remote image format "${K||"unknown"}". Supported: jpg, jpeg, png, gif, webp`)}export async function downloadRemoteImage(q){let J;try{J=new URL(q)}catch{throw new V(`Invalid image URL: ${q}`)}if(!/^https?:$/.test(J.protocol))throw new V(`Unsupported image URL protocol: ${J.protocol}`);const K=await fetch(q);if(!K.ok)throw new V(`Image download failed: HTTP ${K.status}`);const Q=Number(K.headers.get("content-length")||"0");if(Q>MAX_IMAGE_SIZE_BYTES)throw new V(`Image too large: ${(Q/1024/1024).toFixed(1)} MB (max ${MAX_IMAGE_SIZE_BYTES/1024/1024} MB)`);const $=resolveImageExtension(q,K.headers.get("content-type")),j=k.mkdtempSync(F.join(X.tmpdir(),"opencli-twitter-")),z=F.join(j,`image${$}`),v=Buffer.from(await K.arrayBuffer());if(v.byteLength>MAX_IMAGE_SIZE_BYTES){k.rmSync(j,{recursive:!0,force:!0});throw new V(`Image too large: ${(v.byteLength/1024/1024).toFixed(1)} MB (max ${MAX_IMAGE_SIZE_BYTES/1024/1024} MB)`)}k.writeFileSync(z,v);return{absPath:z,cleanupDir:j}}export async function attachComposerImage(q,J,K=COMPOSER_FILE_INPUT_SELECTOR){let Q=!1;if(q.setFileInput)try{await q.setFileInput([J],K);Q=!0}catch(j){const z=j instanceof Error?j.message:String(j);if(!isRecoverableFileInputError(z))throw Error(`Image upload failed: ${z}`)}if(!Q){const j=F.extname(J).toLowerCase(),z=j===".png"?"image/png":j===".gif"?"image/gif":j===".webp"?"image/webp":"image/jpeg",v=k.readFileSync(J).toString("base64");if(v.length>500000)console.warn(`[warn] Image base64 payload is ${(v.length/1024/1024).toFixed(1)}MB. This may fail with the browser bridge. Update the extension to v1.6+ for CDP-based upload, or compress the image before attaching.`);const Z=await q.evaluate(`
2
- (() => {
3
- const input = document.querySelector(${JSON.stringify(K)});
4
- if (!input) return { ok: false, error: 'No file input found on page' };
5
-
6
- const binary = atob(${JSON.stringify(v)});
7
- const bytes = new Uint8Array(binary.length);
8
- for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
9
-
10
- const dt = new DataTransfer();
11
- const blob = new Blob([bytes], { type: ${JSON.stringify(z)} });
12
- dt.items.add(new File([blob], ${JSON.stringify(F.basename(J))}, { type: ${JSON.stringify(z)} }));
13
-
14
- let assigned = false;
15
- try {
16
- Object.defineProperty(input, 'files', { value: dt.files, writable: false, configurable: true });
17
- assigned = input.files && input.files.length > 0;
18
- } catch(e) {
19
- // files property not redefinable — use nativeInputValueSetter trick
20
- try {
21
- const nativeInputFileSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'files');
22
- if (nativeInputFileSetter && nativeInputFileSetter.set) {
23
- nativeInputFileSetter.set.call(input, dt.files);
24
- assigned = input.files && input.files.length > 0;
25
- }
26
- } catch(e2) { /* ignore */ }
27
- }
28
- if (!assigned) return { ok: false, error: 'Could not assign files to input' };
29
- input.dispatchEvent(new Event('change', { bubbles: true }));
30
- input.dispatchEvent(new Event('input', { bubbles: true }));
31
- return { ok: true };
32
- })()
33
- `);if(!Z?.ok)throw Error(`Image upload failed: ${Z?.error??"unknown error"}`)}const $=await waitForComposerMediaReady(q,1);if(!$?.ok)throw Error($?.message??"Image upload failed: preview did not appear.")}export function isRecoverableFileInputError(q){const J=q instanceof Error?q.message:String(q);return/unknown action|not supported|not[-\s]?allowed|notallowederror/i.test(J)}export async function waitForComposerMediaReady(q,J=1){const K=Math.ceil(W/B);return q.evaluate(`
1
+ import*as z from"node:fs";import*as Y from"node:os";import*as F from"node:path";import{ArgumentError as V}from"@jackwener/opencli/errors";import{setInputFiles as R}from"../_shared/file-inject.js";export const TWITTER_BEARER_TOKEN="AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA",COMPOSER_FILE_INPUT_SELECTOR='input[type="file"][data-testid="fileInput"]',SUPPORTED_IMAGE_EXTENSIONS=new Set([".jpg",".jpeg",".png",".gif",".webp"]),MAX_IMAGE_SIZE_BYTES=20971520;const Z=500,B=30000,W={"image/jpeg":".jpg","image/jpg":".jpg","image/png":".png","image/gif":".gif","image/webp":".webp"};export function resolveImagePath(q){const J=F.resolve(q);if(!z.existsSync(J))throw new V(`Image file not found: ${J}`);const K=F.extname(J).toLowerCase();if(!SUPPORTED_IMAGE_EXTENSIONS.has(K))throw new V(`Unsupported image format "${K}". Supported: jpg, jpeg, png, gif, webp`);const Q=z.statSync(J);if(Q.size>MAX_IMAGE_SIZE_BYTES)throw new V(`Image too large: ${(Q.size/1024/1024).toFixed(1)} MB (max ${MAX_IMAGE_SIZE_BYTES/1024/1024} MB)`);return J}export function resolveImageExtension(q,J){const K=(J||"").split(";")[0].trim().toLowerCase();if(K&&W[K])return W[K];try{const Q=new URL(q).pathname,$=F.extname(Q).toLowerCase();if(SUPPORTED_IMAGE_EXTENSIONS.has($))return $}catch{}throw new V(`Unsupported remote image format "${K||"unknown"}". Supported: jpg, jpeg, png, gif, webp`)}export async function downloadRemoteImage(q){let J;try{J=new URL(q)}catch{throw new V(`Invalid image URL: ${q}`)}if(!/^https?:$/.test(J.protocol))throw new V(`Unsupported image URL protocol: ${J.protocol}`);const K=await fetch(q);if(!K.ok)throw new V(`Image download failed: HTTP ${K.status}`);const Q=Number(K.headers.get("content-length")||"0");if(Q>MAX_IMAGE_SIZE_BYTES)throw new V(`Image too large: ${(Q/1024/1024).toFixed(1)} MB (max ${MAX_IMAGE_SIZE_BYTES/1024/1024} MB)`);const $=resolveImageExtension(q,K.headers.get("content-type")),j=z.mkdtempSync(F.join(Y.tmpdir(),"opencli-twitter-")),H=F.join(j,`image${$}`),k=Buffer.from(await K.arrayBuffer());if(k.byteLength>MAX_IMAGE_SIZE_BYTES){z.rmSync(j,{recursive:!0,force:!0});throw new V(`Image too large: ${(k.byteLength/1024/1024).toFixed(1)} MB (max ${MAX_IMAGE_SIZE_BYTES/1024/1024} MB)`)}z.writeFileSync(H,k);return{absPath:H,cleanupDir:j}}export async function attachComposerImage(q,J,K=COMPOSER_FILE_INPUT_SELECTOR){try{await R(q,K,[J])}catch($){throw Error(`Image upload failed: ${$ instanceof Error?$.message:String($)}`)}const Q=await waitForComposerMediaReady(q,1);if(!Q?.ok)throw Error(Q?.message??"Image upload failed: preview did not appear.")}export function isRecoverableFileInputError(q){const J=q instanceof Error?q.message:String(q);return/unknown action|not supported|not[-\s]?allowed|notallowederror/i.test(J)}export async function waitForComposerMediaReady(q,J=1){const K=Math.ceil(B/Z);return q.evaluate(`
34
2
  (async () => {
35
3
  const expected = ${JSON.stringify(J)};
36
4
  for (let i = 0; i < ${JSON.stringify(K)}; i++) {
37
- await new Promise(r => setTimeout(r, ${JSON.stringify(B)}));
5
+ await new Promise(r => setTimeout(r, ${JSON.stringify(Z)}));
38
6
  const previewCount = document.querySelectorAll(
39
7
  '[data-testid="attachments"] img, [data-testid="attachments"] video, [data-testid="attachments"] [role="group"], [data-testid="tweetPhoto"]'
40
8
  ).length;
@@ -50,6 +18,6 @@ import*as k from"node:fs";import*as X from"node:os";import*as F from"node:path";
50
18
  );
51
19
  if (explicitPreviewCount >= expected) return { ok: true, previewCount: explicitPreviewCount };
52
20
  }
53
- return { ok: false, message: 'Image upload timed out (${W/1000}s).' };
21
+ return { ok: false, message: 'Image upload timed out (${B/1000}s).' };
54
22
  })()
55
- `)}const H=Object.freeze({likes:1,retweets:3,replies:2,bookmarks:5,viewsLog:0.5});export function computeEngagementScore(q){if(!q||typeof q!=="object")return 0;const J=(Q)=>{const $=q[Q];if($===void 0||$===null)return 0;const j=Number($);return Number.isFinite(j)?Math.max(0,j):0},K=J("likes")*H.likes+J("retweets")*H.retweets+J("replies")*H.replies+J("bookmarks")*H.bookmarks+Math.log10(J("views")+1)*H.viewsLog;return Math.round(K*100)/100}export function applyTopByEngagement(q,J){if(!Array.isArray(q)||q.length===0)return q;const K=Number(J);if(!Number.isFinite(K)||K<=0)return q;return q.map((Q,$)=>({row:Q,idx:$,score:computeEngagementScore(Q)})).sort((Q,$)=>$.score-Q.score||Q.idx-$.idx).slice(0,Math.floor(K)).map((Q)=>Q.row)}export const __test__={resolveImagePath,resolveImageExtension,downloadRemoteImage,attachComposerImage,isRecoverableFileInputError,waitForComposerMediaReady,computeEngagementScore,applyTopByEngagement,ENGAGEMENT_WEIGHTS:H};
23
+ `)}const v=Object.freeze({likes:1,retweets:3,replies:2,bookmarks:5,viewsLog:0.5});export function computeEngagementScore(q){if(!q||typeof q!=="object")return 0;const J=(Q)=>{const $=q[Q];if($===void 0||$===null)return 0;const j=Number($);return Number.isFinite(j)?Math.max(0,j):0},K=J("likes")*v.likes+J("retweets")*v.retweets+J("replies")*v.replies+J("bookmarks")*v.bookmarks+Math.log10(J("views")+1)*v.viewsLog;return Math.round(K*100)/100}export function applyTopByEngagement(q,J){if(!Array.isArray(q)||q.length===0)return q;const K=Number(J);if(!Number.isFinite(K)||K<=0)return q;return q.map((Q,$)=>({row:Q,idx:$,score:computeEngagementScore(Q)})).sort((Q,$)=>$.score-Q.score||Q.idx-$.idx).slice(0,Math.floor(K)).map((Q)=>Q.row)}export const __test__={resolveImagePath,resolveImageExtension,downloadRemoteImage,attachComposerImage,isRecoverableFileInputError,waitForComposerMediaReady,computeEngagementScore,applyTopByEngagement,ENGAGEMENT_WEIGHTS:v};
@@ -1,4 +1,4 @@
1
- import*as v from"node:fs";import*as L from"node:path";import{cli as w,Strategy as E}from"@jackwener/opencli/registry";import{ArgumentError as Q,AuthRequiredError as c,CommandExecutionError as y}from"@jackwener/opencli/errors";const R="https://channels.weixin.qq.com/platform/post/create",n=['input[placeholder*="短标题"]','input[placeholder*="填写短标题"]','input.weui-desktop-form__input[placeholder*="短标题"]',"input.weui-desktop-form__input"],m=['div[contenteditable][data-placeholder="添加描述"]','div.input-editor[contenteditable=""][data-placeholder="添加描述"]','div[data-placeholder*="描述"][contenteditable]',"div.input-editor[contenteditable]"],d=["span.add-icon.weui-icon-outlined-add","div.upload-content",".finder-video-upload-btn"],u=new Set([".mp4",".mov",".avi",".webm"]);function o(z){if(z&&typeof z==="object"&&"data"in z&&"session"in z)return z.data;return z}async function W(z,J){return o(await z.evaluate(J))}function M(z,J,X){const Z=L.resolve(String(z));let $;try{$=v.statSync(Z)}catch{throw new Q(`${J}文件不存在: ${Z}`)}if(!$.isFile())throw new Q(`${J}路径不是文件: ${Z}`);const V=L.extname(Z).toLowerCase();if(!X.has(V))throw new Q(`不支持的${J}格式: ${V}(支持 ${Array.from(X).join("/")})`);return Z}function S(z){const J=z==null||z===""?600:Number(z);if(!Number.isInteger(J)||J<30)throw new Q("--timeout must be an integer >= 30 seconds");return J}function _(z){if(!z)return null;const J=typeof z==="number"?new Date(z<1000000000000?z*1000:z):new Date(String(z));if(Number.isNaN(J.getTime()))throw new Q(`无法解析定时时间: ${z}`);if(J.getTime()<=Date.now())throw new Q("定时时间必须晚于当前时间");return J}function T(z){return z===!0||z==="true"||z==="1"||z===1}function i(z,J){const X=z-Date.now();if(X<=0)throw new y(`${J}超时,请增加 --timeout 后重试`);return X}function k({isDraft:z,finalUrl:J,successMsg:X}){const Z=String(X||"");if(z)return/草稿已保存|暂存成功|保存成功/.test(Z);if(/已发表|发布成功|发表成功|审核中/.test(Z))return!0;const $=String(J||"");return/\/platform\/post\/list\b/.test($)}export const __test__={parseTimeoutSeconds:S,parseScheduleDate:_,parseBooleanFlag:T,requireFilePath:M,submitSucceeded:k};const H=`
1
+ import*as f from"node:fs";import*as T from"node:path";import{cli as x,Strategy as P}from"@jackwener/opencli/registry";import{ArgumentError as H,AuthRequiredError as w,CommandExecutionError as q}from"@jackwener/opencli/errors";import{setInputFiles as E}from"../_shared/file-inject.js";const A="https://channels.weixin.qq.com/platform/post/create",c=['input[placeholder*="短标题"]','input[placeholder*="填写短标题"]','input.weui-desktop-form__input[placeholder*="短标题"]',"input.weui-desktop-form__input"],m=['div[contenteditable][data-placeholder="添加描述"]','div.input-editor[contenteditable=""][data-placeholder="添加描述"]','div[data-placeholder*="描述"][contenteditable]',"div.input-editor[contenteditable]"],n=["span.add-icon.weui-icon-outlined-add","div.upload-content",".finder-video-upload-btn"],d=new Set([".mp4",".mov",".avi",".webm"]);function u(z){if(z&&typeof z==="object"&&"data"in z&&"session"in z)return z.data;return z}async function j(z,J){return u(await z.evaluate(J))}function N(z,J,Z){const K=T.resolve(String(z));let $;try{$=f.statSync(K)}catch{throw new H(`${J}文件不存在: ${K}`)}if(!$.isFile())throw new H(`${J}路径不是文件: ${K}`);const V=T.extname(K).toLowerCase();if(!Z.has(V))throw new H(`不支持的${J}格式: ${V}(支持 ${Array.from(Z).join("/")})`);return K}function v(z){const J=z==null||z===""?600:Number(z);if(!Number.isInteger(J)||J<30)throw new H("--timeout must be an integer >= 30 seconds");return J}function k(z){if(!z)return null;const J=typeof z==="number"?new Date(z<1000000000000?z*1000:z):new Date(String(z));if(Number.isNaN(J.getTime()))throw new H(`无法解析定时时间: ${z}`);if(J.getTime()<=Date.now())throw new H("定时时间必须晚于当前时间");return J}function R(z){return z===!0||z==="true"||z==="1"||z===1}function o(z,J){const Z=z-Date.now();if(Z<=0)throw new q(`${J}超时,请增加 --timeout 后重试`);return Z}function M({isDraft:z,finalUrl:J,successMsg:Z}){const K=String(Z||"");if(z)return/草稿已保存|暂存成功|保存成功/.test(K);if(/已发表|发布成功|发表成功|审核中/.test(K))return!0;const $=String(J||"");return/\/platform\/post\/list\b/.test($)}export const __test__={parseTimeoutSeconds:v,parseScheduleDate:k,parseBooleanFlag:R,requireFilePath:N,submitSucceeded:M};const y=`
2
2
  function wujieRoot() {
3
3
  var w = document.querySelector('wujie-app');
4
4
  return (w && w.shadowRoot) || null;
@@ -25,10 +25,10 @@ import*as v from"node:fs";import*as L from"node:path";import{cli as w,Strategy a
25
25
  var rect = el.getBoundingClientRect();
26
26
  return rect.width > 0 && rect.height > 0;
27
27
  }
28
- `;async function D(z){return await W(z,`
28
+ `;async function i(z){return await j(z,`
29
29
  (() => {
30
- ${H}
31
- var sels = ${JSON.stringify(d)};
30
+ ${y}
31
+ var sels = ${JSON.stringify(n)};
32
32
  for (var i = 0; i < sels.length; i++) {
33
33
  var el = deepQuery(sels[i]);
34
34
  if (el && isVisible(el)) {
@@ -38,47 +38,19 @@ import*as v from"node:fs";import*as L from"node:path";import{cli as w,Strategy a
38
38
  }
39
39
  return { ok: false };
40
40
  })()
41
- `)}async function b(z,J=15000){const X=500,Z=Math.max(1,Math.ceil(J/X));for(let $=0;$<Z;$++){if(await W(z,`
41
+ `)}async function p(z,J=15000){const Z=500,K=Math.max(1,Math.ceil(J/Z));for(let $=0;$<K;$++){if(await j(z,`
42
42
  (() => {
43
- ${H}
43
+ ${y}
44
44
  var inputSels = ['input[type="file"][accept*="video"]', 'input[type="file"]'];
45
45
  for (var i = 0; i < inputSels.length; i++) {
46
46
  if (deepQuery(inputSels[i])) return true;
47
47
  }
48
48
  return false;
49
49
  })()
50
- `))return!0;if($<Z-1)await z.wait({time:X/1000})}return!1}async function p(z,J){if(z.setFileInput){await D(z);await z.wait({time:1});await b(z);for(const q of['input[type="file"][accept*="video"]','input[type="file"]'])try{await z.setFileInput([J],q);return}catch(O){}}const Z=v.readFileSync(J).toString("base64"),$=L.basename(J),V=L.extname(J).toLowerCase(),j={".mp4":"video/mp4",".mov":"video/quicktime",".avi":"video/avi",".webm":"video/webm"}[V]||"video/mp4";await W(z,"() => { window.__oc_chunks = []; }");const G=50000;for(let q=0;q<Z.length;q+=G){const O=Z.slice(q,q+G);await W(z,`((c) => { window.__oc_chunks.push(c); })(${JSON.stringify(O)})`)}await D(z);await z.wait({time:0.5});await b(z);const B=await W(z,`
51
- (function(params) {
52
- ${H}
53
- var inputSels = ['input[type="file"][accept*="video"]', 'input[type="file"]'];
54
- var input = null;
55
- for (var i = 0; i < inputSels.length; i++) {
56
- input = deepQuery(inputSels[i]);
57
- if (input) break;
58
- }
59
- if (!input) { window.__oc_chunks = []; return { ok: false, error: 'No file input found' }; }
60
-
61
- try {
62
- var b64 = window.__oc_chunks.join('');
63
- window.__oc_chunks = [];
64
- var binary = atob(b64);
65
- var bytes = new Uint8Array(binary.length);
66
- for (var i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
67
- var dt = new DataTransfer();
68
- dt.items.add(new File([bytes], params.fileName, { type: params.mimeType }));
69
- Object.defineProperty(input, 'files', { value: dt.files, configurable: true });
70
- input.dispatchEvent(new Event('change', { bubbles: true }));
71
- input.dispatchEvent(new Event('input', { bubbles: true }));
72
- return { ok: true };
73
- } catch(e) {
74
- window.__oc_chunks = [];
75
- return { ok: false, error: e.message };
76
- }
77
- })(${JSON.stringify({fileName:$,mimeType:j})})
78
- `);if(!B?.ok){await z.screenshot({path:"/tmp/wechat-channels_publish_upload_debug.png"});throw new y(`视频文件注入失败: ${B?.error??"unknown"}
79
- 截图已保存到 /tmp/wechat-channels_publish_upload_debug.png`)}}async function s(z,J,X=180000){const Z=3000,$=Math.ceil(X/Z);for(let V=0;V<$;V++){let K;try{K=await W(z,`
50
+ `))return!0;if($<K-1)await z.wait({time:Z/1000})}return!1}async function g(z,J){await i(z);await z.wait({time:1});await p(z);try{await E(z,['input[type="file"][accept*="video"]','input[type="file"]'],[J],{findInputJs:`((sel) => { ${y} return deepQuery(sel); })`})}catch(Z){await z.screenshot({path:"/tmp/wechat-channels_publish_upload_debug.png"});throw new q(`视频文件注入失败: ${Z instanceof Error?Z.message:String(Z)}
51
+ 截图已保存到 /tmp/wechat-channels_publish_upload_debug.png`)}}async function s(z,J,Z=180000){const K=3000,$=Math.ceil(Z/K);for(let V=0;V<$;V++){let W;try{W=await j(z,`
80
52
  ((fileName) => {
81
- ${H}
53
+ ${y}
82
54
  var root = wujieRoot() || document;
83
55
  var bodyText = (root.innerText || root.textContent || '').trim();
84
56
  var uploading = deepQuery('[class*="upload"][class*="progress"]') ||
@@ -98,10 +70,10 @@ import*as v from"node:fs";import*as L from"node:path";import{cli as w,Strategy a
98
70
  var hasSuccessText = /上传成功|转码完成|处理完成/.test(bodyText);
99
71
  return { done: !uploading && (!!preview || hasFileEvidence || hasSuccessText), failed: false };
100
72
  })(${JSON.stringify(J)})
101
- `)}catch(j){const G=j instanceof Error?j.message:String(j);if(G.includes("ECONNRESET")||G.includes("fetch failed")){process.stderr.write(` [retry] bridge reconnecting after page re-render (${V+1}/${$})...
102
- `);await z.wait({time:Z/1000});continue}throw j}if(K?.failed)throw new y("视频上传失败,请检查文件格式和网络连接");if(K?.done)return;await z.wait({time:Z/1000})}throw new y(`视频上传/转码超时(${Math.ceil(X/1000)}秒),请检查网络或稍后重试`)}async function U(z,J,X,Z){const $=30;let V;for(let K=0;K<$;K++){V=await W(z,`
73
+ `)}catch(X){const G=X instanceof Error?X.message:String(X);if(G.includes("ECONNRESET")||G.includes("fetch failed")){process.stderr.write(` [retry] bridge reconnecting after page re-render (${V+1}/${$})...
74
+ `);await z.wait({time:K/1000});continue}throw X}if(W?.failed)throw new q("视频上传失败,请检查文件格式和网络连接");if(W?.done)return;await z.wait({time:K/1000})}throw new q(`视频上传/转码超时(${Math.ceil(Z/1000)}秒),请检查网络或稍后重试`)}async function S(z,J,Z,K){const $=30;let V;for(let W=0;W<$;W++){V=await j(z,`
103
75
  (function(selectors, text) {
104
- ${H}
76
+ ${y}
105
77
 
106
78
  var el = null;
107
79
  var foundSel = null;
@@ -145,10 +117,10 @@ import*as v from"node:fs";import*as L from"node:path";import{cli as w,Strategy a
145
117
  var actual = el.isContentEditable ? (el.innerText || el.textContent || '') : (el.value || '');
146
118
  el.blur();
147
119
  return { ok: actual.indexOf(text) >= 0, sel: foundSel, actual: actual };
148
- })(${JSON.stringify(J)}, ${JSON.stringify(X)})
149
- `);if(V?.ok)break;if(K<$-1)await z.wait({time:0.5})}if(!V?.ok){await z.screenshot({path:`/tmp/wechat-channels_publish_${Z}_debug.png`});throw new y(`找不到 ${Z} 输入框,截图已保存到 /tmp/wechat-channels_publish_${Z}_debug.png`)}}async function g(z,J){const X=J.getFullYear(),Z=J.getMonth()+1,$=J.getDate(),V=J.getHours(),K=J.getMinutes(),j=(q)=>String(q).padStart(2,"0"),G=await W(z,`
120
+ })(${JSON.stringify(J)}, ${JSON.stringify(Z)})
121
+ `);if(V?.ok)break;if(W<$-1)await z.wait({time:0.5})}if(!V?.ok){await z.screenshot({path:`/tmp/wechat-channels_publish_${K}_debug.png`});throw new q(`找不到 ${K} 输入框,截图已保存到 /tmp/wechat-channels_publish_${K}_debug.png`)}}async function l(z,J){const Z=J.getFullYear(),K=J.getMonth()+1,$=J.getDate(),V=J.getHours(),W=J.getMinutes(),X=(O)=>String(O).padStart(2,"0"),G=await j(z,`
150
122
  (async function(TY, TM, TD, TH, TMin) {
151
- ${H}
123
+ ${y}
152
124
  function sleep(ms) { return new Promise(function(r) { setTimeout(r, ms); }); }
153
125
  function click(el) {
154
126
  el.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, cancelable: true, view: window }));
@@ -246,11 +218,11 @@ import*as v from"node:fs";import*as L from"node:path";import{cli as w,Strategy a
246
218
  // 6. Read back the display input to confirm the value landed.
247
219
  var inp = deepQuery('input[placeholder*="发表时间"]');
248
220
  return { ok: true, value: inp ? inp.value : null };
249
- })(${X}, ${Z}, ${$}, ${V}, ${K})
250
- `);if(!G?.ok){await z.screenshot({path:"/tmp/wechat-channels_schedule_debug.png"});const q=G?.reason?String(G.reason):"empty picker result";throw new y(`定时设置失败 (${q}),截图: /tmp/wechat-channels_schedule_debug.png`)}const B=`${X}-${j(Z)}-${j($)} ${j(V)}:${j(K)}`;if(!String(G.value||"").includes(B))throw new y(`定时设置未验证成功: expected=${B} actual=${G.value||""}`);process.stderr.write(` 定时设置完成: ${G.value||B}
251
- `)}async function l(z,J){const X=J?["存草稿","保存草稿","草稿"]:["发表","发布"],Z=30;let $;for(let V=0;V<Z;V++){$=await W(z,`
221
+ })(${Z}, ${K}, ${$}, ${V}, ${W})
222
+ `);if(!G?.ok){await z.screenshot({path:"/tmp/wechat-channels_schedule_debug.png"});const O=G?.reason?String(G.reason):"empty picker result";throw new q(`定时设置失败 (${O}),截图: /tmp/wechat-channels_schedule_debug.png`)}const B=`${Z}-${X(K)}-${X($)} ${X(V)}:${X(W)}`;if(!String(G.value||"").includes(B))throw new q(`定时设置未验证成功: expected=${B} actual=${G.value||""}`);process.stderr.write(` 定时设置完成: ${G.value||B}
223
+ `)}async function t(z,J){const Z=J?["存草稿","保存草稿","草稿"]:["发表","发布"],K=30;let $;for(let V=0;V<K;V++){$=await j(z,`
252
224
  (function(labels) {
253
- ${H}
225
+ ${y}
254
226
  var btns = deepQueryAll('button');
255
227
  for (var i = 0; i < btns.length; i++) {
256
228
  var btn = btns[i];
@@ -267,22 +239,22 @@ import*as v from"node:fs";import*as L from"node:path";import{cli as w,Strategy a
267
239
  }
268
240
  }
269
241
  return { ok: false };
270
- })(${JSON.stringify(X)})
271
- `);if($?.ok)break;if(V<Z-1)await z.wait({time:0.5})}if(!$?.ok){await z.screenshot({path:"/tmp/wechat-channels_publish_submit_debug.png"});throw new y(`找不到"${X[0]}"按钮(按钮可能被禁用或表单未完成),`+"截图已保存到 /tmp/wechat-channels_publish_submit_debug.png")}return $}w({site:"wechat-channels",name:"publish",access:"write",description:"发布视频到视频号",domain:"channels.weixin.qq.com",strategy:E.COOKIE,browser:!0,navigateBefore:!1,args:[{name:"video",required:!0,positional:!0,help:"视频文件路径 (.mp4/.mov/.avi/.webm)"},{name:"title",required:!1,help:"短标题(建议 6-16 字)"},{name:"caption",required:!1,help:"描述内容,支持直接写 #话题(如:日常生活 #搞笑 #生活)"},{name:"schedule",required:!1,help:'定时发布时间(ISO8601 或 Unix 秒,如 "2026-05-20 10:00")'},{name:"draft",type:"bool",default:!1,help:"保存为草稿"},{name:"manual",type:"bool",default:!1,help:"填完所有字段后不自动发布,由用户手动点击发表(务必同时传 --site-session persistent,否则表单页约 30 秒后会被重置为空白页)"},{name:"timeout",type:"int",required:!1,default:600,help:"命令整体超时秒数(含登录等待 + 上传转码,默认 600)"}],columns:["status","title","detail"],func:async(z,J)=>{if(!z)throw new y("需要浏览器页面");const X=S(J.timeout),Z=Date.now()+X*1000,$=M(J.video,"视频",u),V=String(J.title??"").trim(),K=String(J.caption??"").trim(),j=_(J.schedule||null),G=T(J.draft),B=T(J.manual);await z.goto(R);await z.wait({time:4});{const Y=(C)=>typeof C==="string"&&C.includes("/platform/post/create"),F=await W(z,"() => location.href");if(!Y(F)){process.stderr.write(`
242
+ })(${JSON.stringify(Z)})
243
+ `);if($?.ok)break;if(V<K-1)await z.wait({time:0.5})}if(!$?.ok){await z.screenshot({path:"/tmp/wechat-channels_publish_submit_debug.png"});throw new q(`找不到"${Z[0]}"按钮(按钮可能被禁用或表单未完成),`+"截图已保存到 /tmp/wechat-channels_publish_submit_debug.png")}return $}x({site:"wechat-channels",name:"publish",access:"write",description:"发布视频到视频号",domain:"channels.weixin.qq.com",strategy:P.COOKIE,browser:!0,navigateBefore:!1,args:[{name:"video",required:!0,positional:!0,help:"视频文件路径 (.mp4/.mov/.avi/.webm)"},{name:"title",required:!1,help:"短标题(建议 6-16 字)"},{name:"caption",required:!1,help:"描述内容,支持直接写 #话题(如:日常生活 #搞笑 #生活)"},{name:"schedule",required:!1,help:'定时发布时间(ISO8601 或 Unix 秒,如 "2026-05-20 10:00")'},{name:"draft",type:"bool",default:!1,help:"保存为草稿"},{name:"manual",type:"bool",default:!1,help:"填完所有字段后不自动发布,由用户手动点击发表(务必同时传 --site-session persistent,否则表单页约 30 秒后会被重置为空白页)"},{name:"timeout",type:"int",required:!1,default:600,help:"命令整体超时秒数(含登录等待 + 上传转码,默认 600)"}],columns:["status","title","detail"],func:async(z,J)=>{if(!z)throw new q("需要浏览器页面");const Z=v(J.timeout),K=Date.now()+Z*1000,$=N(J.video,"视频",d),V=String(J.title??"").trim(),W=String(J.caption??"").trim(),X=k(J.schedule||null),G=R(J.draft),B=R(J.manual);await z.goto(A);await z.wait({time:4});{const Y=(I)=>typeof I==="string"&&I.includes("/platform/post/create"),b=await j(z,"() => location.href");if(!Y(b)){process.stderr.write(`
272
244
  ⚠️ 未登录视频号(已被重定向离开发布页)。请在 Chrome 中扫码登录...
273
- `+" 当前地址: "+F+`
245
+ `+" 当前地址: "+b+`
274
246
  `+` 登录完成后将自动继续发布。
275
247
 
276
- `);const C=Math.min(Z,Date.now()+120000);let N=!1;while(Date.now()<C){await z.wait({time:3});await z.goto(R);await z.wait({time:2});const P=await W(z,"() => location.href");if(Y(P)){N=!0;break}process.stderr.write(` 等待扫码中...
277
- `)}if(!N)throw new c("channels.weixin.qq.com","登录超时(2分钟),请手动登录后重试");process.stderr.write(`✅ 登录成功,继续发布...
248
+ `);const I=Math.min(K,Date.now()+120000);let F=!1;while(Date.now()<I){await z.wait({time:3});await z.goto(A);await z.wait({time:2});const h=await j(z,"() => location.href");if(Y(h)){F=!0;break}process.stderr.write(` 等待扫码中...
249
+ `)}if(!F)throw new w("channels.weixin.qq.com","登录超时(2分钟),请手动登录后重试");process.stderr.write(`✅ 登录成功,继续发布...
278
250
 
279
- `);await z.wait({time:3});await z.goto(R);await z.wait({time:5})}}await p(z,$);await z.wait({time:2});await s(z,L.basename($),Math.min(180000,i(Z,"视频上传/转码")));await z.wait({time:1});if(V){await U(z,n,V,"title");await z.wait({time:0.5})}if(K){await U(z,m,K,"caption");await z.wait({time:0.5})}if(j){await g(z,j);await z.wait({time:0.5})}if(B){process.stderr.write(`
251
+ `);await z.wait({time:3});await z.goto(A);await z.wait({time:5})}}await g(z,$);await z.wait({time:2});await s(z,T.basename($),Math.min(180000,o(K,"视频上传/转码")));await z.wait({time:1});if(V){await S(z,c,V,"title");await z.wait({time:0.5})}if(W){await S(z,m,W,"caption");await z.wait({time:0.5})}if(X){await l(z,X);await z.wait({time:0.5})}if(B){process.stderr.write(`
280
252
  ℹ️ 手动模式:表单已填好,请在浏览器中检查并点击「发表」。
281
253
  `+` 若未加 --site-session persistent,此标签页约 30 秒后会被重置为空白页。
282
254
 
283
- `);return[{status:"⏸️ 已填写完毕,请在浏览器中手动点击发表",title:V||"",detail:[j?`定时: ${j}`:null].filter(Boolean).join(" · ")||"表单已就绪"}]}await l(z,G);await z.wait({time:4});const q=G?["草稿已保存","暂存成功","保存成功"]:["已发表","发布成功","发表成功","审核中"];let O="",I="",A=!1;for(let Y=0;Y<30;Y++){O=await W(z,"() => location.href");I=await W(z,`
255
+ `);return[{status:"⏸️ 已填写完毕,请在浏览器中手动点击发表",title:V||"",detail:[X?`定时: ${X}`:null].filter(Boolean).join(" · ")||"表单已就绪"}]}await t(z,G);await z.wait({time:4});const O=G?["草稿已保存","暂存成功","保存成功"]:["已发表","发布成功","发表成功","审核中"];let Q="",L="",C=!1;for(let Y=0;Y<30;Y++){Q=await j(z,"() => location.href");L=await j(z,`
284
256
  (function(markers) {
285
- ${H}
257
+ ${y}
286
258
  var all = deepQueryAll('*');
287
259
  for (var i = 0; i < all.length; i++) {
288
260
  var el = all[i];
@@ -294,5 +266,5 @@ import*as v from"node:fs";import*as L from"node:path";import{cli as w,Strategy a
294
266
  }
295
267
  }
296
268
  return '';
297
- })(${JSON.stringify(q)})
298
- `);A=k({isDraft:G,finalUrl:O,successMsg:I});if(A)break;await z.wait({time:0.5})}if(!A){await z.screenshot({path:"/tmp/wechat-channels_publish_result_debug.png"});throw new y(`未能验证${G?"草稿保存":"发布"}成功,截图已保存到 /tmp/wechat-channels_publish_result_debug.png`,`url=${O||""} message=${I||""}`)}const f=G?"草稿已保存":"发布成功",x=[j?`定时: ${j.toISOString()}`:null,I||O].filter(Boolean),h=[{status:`✅ ${f}`,title:V||"",detail:x.join(" · ")||O}];await z.goto("https://channels.weixin.qq.com/platform/post/list",{waitUntil:"none"}).catch(()=>{});return h}});
269
+ })(${JSON.stringify(O)})
270
+ `);C=M({isDraft:G,finalUrl:Q,successMsg:L});if(C)break;await z.wait({time:0.5})}if(!C){await z.screenshot({path:"/tmp/wechat-channels_publish_result_debug.png"});throw new q(`未能验证${G?"草稿保存":"发布"}成功,截图已保存到 /tmp/wechat-channels_publish_result_debug.png`,`url=${Q||""} message=${L||""}`)}const U=G?"草稿已保存":"发布成功",_=[X?`定时: ${X.toISOString()}`:null,L||Q].filter(Boolean),D=[{status:`✅ ${U}`,title:V||"",detail:_.join(" · ")||Q}];await z.goto("https://channels.weixin.qq.com/platform/post/list",{waitUntil:"none"}).catch(()=>{});return D}});