publishport-opencli 1.0.8 → 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.
- package/cli-manifest.json +442 -1
- package/clis/_shared/file-inject.js +41 -0
- package/clis/_shared/video-publish.js +4 -4
- package/clis/bilibili/upload.js +1 -1
- package/clis/chatgpt/utils.js +5 -5
- package/clis/claude/utils.js +1 -1
- package/clis/deepseek/utils.js +7 -7
- package/clis/douyin/draft.js +6 -6
- package/clis/douyin/publish-image.js +10 -31
- package/clis/facebook/feed.js +78 -5
- package/clis/facebook/post.js +9 -36
- package/clis/facebook/search.js +83 -22
- package/clis/instagram/post.js +19 -66
- package/clis/instagram/reel.js +11 -11
- package/clis/threads/post.js +15 -34
- package/clis/twitter/post.js +15 -42
- package/clis/twitter/utils.js +4 -36
- package/clis/wechat-channels/publish.js +29 -57
- package/clis/weibo/publish.js +8 -8
- package/clis/weixin/create-draft.js +7 -7
- package/clis/xianyu/collect.js +1 -0
- package/clis/xianyu/location.js +1 -0
- package/clis/xianyu/manage.js +1 -0
- package/clis/xianyu/mtop.js +47 -0
- package/clis/xianyu/order.js +1 -0
- package/clis/xianyu/publish.js +2 -2
- package/clis/xianyu/rate.js +1 -0
- package/clis/xianyu/user.js +1 -0
- package/clis/xiaohongshu/publish-video.js +9 -27
- package/clis/xiaohongshu/publish.js +32 -72
- package/dist/src/browser/base-page.d.ts +19 -0
- package/dist/src/browser/base-page.js +1 -1
- package/dist/src/browser/bridge-readiness.d.ts +1 -1
- package/dist/src/browser/daemon-client.d.ts +72 -1
- package/dist/src/browser/daemon-client.js +1 -1
- package/dist/src/browser/daemon-version.d.ts +1 -1
- package/dist/src/browser/errors.d.ts +2 -0
- package/dist/src/browser/errors.js +3 -3
- package/dist/src/browser/network-interceptor.d.ts +11 -0
- package/dist/src/browser/network-interceptor.js +1 -0
- package/dist/src/browser/target-resolver.d.ts +1 -0
- package/dist/src/browser/target-resolver.js +100 -6
- package/dist/src/cli.js +24 -24
- package/dist/src/commands/daemon.js +1 -1
- package/dist/src/daemon-utils.d.ts +1 -0
- package/dist/src/daemon-utils.js +1 -1
- package/dist/src/daemon.js +1 -1
- package/dist/src/doctor.d.ts +1 -1
- package/dist/src/doctor.js +7 -7
- package/dist/src/errors.d.ts +8 -0
- package/dist/src/errors.js +1 -1
- package/dist/src/execution.js +2 -2
- package/dist/src/external.js +1 -1
- package/dist/src/session-lease.d.ts +135 -0
- package/dist/src/session-lease.js +1 -0
- package/dist/src/session-lease.test.d.ts +1 -0
- package/dist/src/types.d.ts +3 -0
- package/package.json +1 -1
package/clis/threads/post.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import*as
|
|
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
|
|
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
|
-
${
|
|
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
|
|
38
|
-
${
|
|
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
|
-
${
|
|
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
|
|
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
|
-
${
|
|
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
|
-
${
|
|
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 (${
|
|
95
|
-
})()`}async function
|
|
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
|
-
${
|
|
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
|
|
120
|
+
`}async function I(z){return z.evaluate(buildSubmitJs())}export function buildPublishStatusProbeJs(){return`
|
|
140
121
|
(() => {
|
|
141
|
-
${
|
|
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
|
|
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:""}]}});
|
package/clis/twitter/post.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import*as
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
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(
|
|
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 (${
|
|
62
|
-
})()`)}async function
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
148
|
-
const r = await fetch(${JSON.stringify(
|
|
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: ${
|
|
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(
|
|
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}:{}}]}});
|
package/clis/twitter/utils.js
CHANGED
|
@@ -1,40 +1,8 @@
|
|
|
1
|
-
import*as
|
|
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(
|
|
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 (${
|
|
21
|
+
return { ok: false, message: 'Image upload timed out (${B/1000}s).' };
|
|
54
22
|
})()
|
|
55
|
-
`)}const
|
|
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
|
|
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
|
|
28
|
+
`;async function i(z){return await j(z,`
|
|
29
29
|
(() => {
|
|
30
|
-
${
|
|
31
|
-
var sels = ${JSON.stringify(
|
|
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
|
|
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
|
-
${
|
|
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($<
|
|
51
|
-
|
|
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
|
-
${
|
|
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(
|
|
102
|
-
`);await z.wait({time:
|
|
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
|
-
${
|
|
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(
|
|
149
|
-
`);if(V?.ok)break;if(
|
|
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
|
-
${
|
|
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
|
-
})(${
|
|
250
|
-
`);if(!G?.ok){await z.screenshot({path:"/tmp/wechat-channels_schedule_debug.png"});const
|
|
251
|
-
`)}async function
|
|
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
|
-
${
|
|
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(
|
|
271
|
-
`);if($?.ok)break;if(V<
|
|
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
|
-
`+" 当前地址: "+
|
|
245
|
+
`+" 当前地址: "+b+`
|
|
274
246
|
`+` 登录完成后将自动继续发布。
|
|
275
247
|
|
|
276
|
-
`);const
|
|
277
|
-
`)}if(!
|
|
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(
|
|
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:[
|
|
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
|
-
${
|
|
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(
|
|
298
|
-
`);
|
|
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}});
|