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/chatgpt/utils.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{htmlToMarkdown as
|
|
1
|
+
import{htmlToMarkdown as m}from"@jackwener/opencli/utils";import{ArgumentError as k,AuthRequiredError as _,CommandExecutionError as F,TimeoutError as O}from"@jackwener/opencli/errors";export const CHATGPT_DOMAIN="chatgpt.com",CHATGPT_URL="https://chatgpt.com";const I={instant:{label:"Instant",labels:["Instant","即时","极速"],optionLabels:["Instant","极速","即时"],testIds:["model-switcher-gpt-5-5"],intelligenceOrder:0},medium:{label:"Medium",labels:["Medium","均衡"],optionLabels:["Medium","均衡"],testIds:[],intelligenceOrder:1},high:{label:"High",labels:["High","高级","Thinking","思考"],optionLabels:["High","高级","Thinking","思考"],testIds:["model-switcher-gpt-5-5-thinking"],intelligenceOrder:2},"extra-high":{label:"Extra High",labels:["Extra High","超高"],optionLabels:["Extra High","超高"],testIds:[],intelligenceOrder:3},pro:{label:"Pro",labels:["Pro","进阶专业","专业"],optionLabels:["专业","Pro","进阶专业"],testIds:["model-switcher-gpt-5-5-pro"],intelligenceOrder:4}},w={thinking:"high"};export const CHATGPT_MODEL_CHOICES=[...Object.keys(I),...Object.keys(w)];const G={"deep-research":{label:"Deep Research",labels:["深度研究","Deep Research"]},"web-search":{label:"Web Search",labels:["网页搜索","搜索","Web Search","Search"]}};export const CHATGPT_TOOL_CHOICES=Object.keys(G);const x=['[contenteditable="true"][role="textbox"]','#prompt-textarea[contenteditable="true"]','[aria-label="Chat with ChatGPT"]','[aria-label="与 ChatGPT 聊天"]','[placeholder="Ask anything"]','[placeholder="有问题,尽管问"]',"#prompt-textarea",'[data-testid="prompt-textarea"]'],U='button[data-testid="send-button"]:not([disabled])',L=["#composer-submit-button:not([disabled])"],y=["Send prompt","Send message","Send","发送","发送消息","发送提示"],A=["Close sidebar","关闭边栏"];function N(Q,X){if(!Q||!X)return!1;return Q===X||Q.startsWith(`${X}?`)||Q.startsWith(`${X}#`)}function v(){return`
|
|
2
2
|
const isVisible = (el) => {
|
|
3
3
|
if (!(el instanceof HTMLElement)) return false;
|
|
4
4
|
const style = window.getComputedStyle(el);
|
|
@@ -475,7 +475,7 @@ import{htmlToMarkdown as i}from"@jackwener/opencli/utils";import{ArgumentError a
|
|
|
475
475
|
rows.push({ role, text, html });
|
|
476
476
|
}
|
|
477
477
|
return rows;
|
|
478
|
-
})()`)),"chatgpt visible messages").map((Y,Z)=>({Index:Z+1,Role:Y?.role==="Assistant"?"Assistant":"User",Text:String(Y?.text||"").trim(),Html:String(Y?.html||"")})).filter((Y)=>Y.Text)}function h(Q,{wantMarkdown:X,generating:Y,stableSeconds:Z}){return Q.map(($)=>({Index:$.Index,Role:$.Role,Text:X&&$.Role==="Assistant"&&$.Html?messageHtmlToMarkdown($.Html)||$.Text:$.Text,Generating:Y,StableSeconds:Z}))}export async function getChatGPTDetailRows(Q,{wantMarkdown:X=!1,stableSeconds:Y=0}={}){const Z=await isGenerating(Q),$=await getVisibleMessages(Q);return{messages:$,rows:h($,{wantMarkdown:X,generating:Z,stableSeconds:Y}),generating:Z}}export async function waitForChatGPTDetailRows(Q,{wantMarkdown:X=!1,timeoutSeconds:Y=120,stableSeconds:Z=6}={}){const $=Date.now();let W="",V=0;while(Date.now()-$<Y*1000){const z=await isGenerating(Q),q=await getVisibleMessages(Q),j=JSON.stringify(q.map((J)=>[J.Role,J.Text]));if(!z&&q.length&&q[q.length-1]?.Role==="Assistant")if(j===W){if(!V)V=Date.now();const J=Math.floor((Date.now()-V)/1000);if(J>=Z)return{messages:q,rows:h(q,{wantMarkdown:X,generating:!1,stableSeconds:J}),generating:!1}}else{W=j;V=Date.now()}else{W=j;V=0}await Q.wait(3)}throw new O("chatgpt detail",Y,"Conversation did not finish or stabilize before timeout. Re-run with a higher --timeout if it is still generating.")}export function messageHtmlToMarkdown(Q){try{return
|
|
478
|
+
})()`)),"chatgpt visible messages").map((Y,Z)=>({Index:Z+1,Role:Y?.role==="Assistant"?"Assistant":"User",Text:String(Y?.text||"").trim(),Html:String(Y?.html||"")})).filter((Y)=>Y.Text)}function h(Q,{wantMarkdown:X,generating:Y,stableSeconds:Z}){return Q.map(($)=>({Index:$.Index,Role:$.Role,Text:X&&$.Role==="Assistant"&&$.Html?messageHtmlToMarkdown($.Html)||$.Text:$.Text,Generating:Y,StableSeconds:Z}))}export async function getChatGPTDetailRows(Q,{wantMarkdown:X=!1,stableSeconds:Y=0}={}){const Z=await isGenerating(Q),$=await getVisibleMessages(Q);return{messages:$,rows:h($,{wantMarkdown:X,generating:Z,stableSeconds:Y}),generating:Z}}export async function waitForChatGPTDetailRows(Q,{wantMarkdown:X=!1,timeoutSeconds:Y=120,stableSeconds:Z=6}={}){const $=Date.now();let W="",V=0;while(Date.now()-$<Y*1000){const z=await isGenerating(Q),q=await getVisibleMessages(Q),j=JSON.stringify(q.map((J)=>[J.Role,J.Text]));if(!z&&q.length&&q[q.length-1]?.Role==="Assistant")if(j===W){if(!V)V=Date.now();const J=Math.floor((Date.now()-V)/1000);if(J>=Z)return{messages:q,rows:h(q,{wantMarkdown:X,generating:!1,stableSeconds:J}),generating:!1}}else{W=j;V=Date.now()}else{W=j;V=0}await Q.wait(3)}throw new O("chatgpt detail",Y,"Conversation did not finish or stabilize before timeout. Re-run with a higher --timeout if it is still generating.")}export function messageHtmlToMarkdown(Q){try{return m(Q).trim()}catch{return String(Q||"").replace(/<[^>]+>/g," ").replace(/\s+/g," ").trim()}}export async function getBubbleCount(Q){return(await getVisibleMessages(Q)).length}function M(Q){return String(Q||"").replace(/\s+/g," ").trim()}function E(Q,X){return JSON.stringify([M(Q?.Text),String(X?.Text||"").trim()])}export function getChatGPTResponsePairKeys(Q,X){const Y=M(X);if(!Y)return[];const Z=[];for(let $=0;$<Q.length;$+=1){const W=Q[$];if(W?.Role!=="User"||M(W.Text)!==Y)continue;const V=Q.slice($+1).find((z)=>z?.Role==="Assistant");if(!V||!String(V.Text||"").trim())continue;Z.push(E(W,V))}return Z}export function getChatGPTResponsePairCounts(Q,X){const Y=new Map;for(const Z of getChatGPTResponsePairKeys(Q,X))Y.set(Z,(Y.get(Z)||0)+1);return Y}function l(Q){if(Q.baselinePairCounts instanceof Map)return Q.baselinePairCounts;return new Map(Array.from(Q.baselinePairKeys||[]).map((X)=>[X,1]))}function r(Q,X,Y){const Z=M(X);if(!Z)return"";const $=getChatGPTResponsePairCounts(Q,X);for(let W=Q.length-1;W>=0;W-=1){const V=Q[W];if(V?.Role!=="User"||M(V.Text)!==Z)continue;const z=Q.findIndex((J,K)=>K>W&&J?.Role==="Assistant"&&String(J.Text||"").trim());if(z<0)continue;const q=Q[z],j=E(V,q);if(($.get(j)||0)<=(Y.get(j)||0))continue;return String(q.Text||"").trim()}return""}export async function waitForChatGPTResponse(Q,X,Y,Z,$={}){const W=Date.now();let V="",z=0;const q=l($);while(Date.now()-W<Z*1000){await Q.wait(3);if($.conversationUrl){const K=await currentChatGPTUrl(Q);if(K&&!N(K,$.conversationUrl))throw new F(`ChatGPT navigated away from the target conversation (${$.conversationUrl}); current URL is ${K}`)}if(await isGenerating(Q)){z=0;continue}const j=await getVisibleMessages(Q),J=r(j,Y,q);if(!J||J===String(Y||"").trim())continue;if(J===V){z+=1;if(z>=2)return J}else{V=J;z=0}}throw new O("chatgpt ask",Z,"No ChatGPT response appeared before timeout. Re-run with a higher --timeout if it is still generating.")}export async function getConversationList(Q){await ensureOnChatGPT(Q);if(requireBooleanEvaluateResult(unwrapEvaluateResult(await Q.evaluate(`(() => {
|
|
479
479
|
const button = Array.from(document.querySelectorAll('button'))
|
|
480
480
|
.find((node) => /open sidebar/i.test(node.getAttribute('aria-label') || ''));
|
|
481
481
|
if (button instanceof HTMLElement) {
|
|
@@ -535,7 +535,7 @@ import{htmlToMarkdown as i}from"@jackwener/opencli/utils";import{ArgumentError a
|
|
|
535
535
|
const previewNodes = Array.from(scope.querySelectorAll('img[src], canvas, video, [style*="background-image"]')).filter(isVisibleMedia);
|
|
536
536
|
return previewNodes.length >= names.length;
|
|
537
537
|
})()
|
|
538
|
-
`)),"chatgpt upload preview detection"))return!0}return!1}export async function uploadChatGPTImages(Q,X){const Y=await import("node:fs"),Z=await import("node:path"),$=await prepareChatGPTImagePaths(X);if(!$.ok)return $;const W=$.paths,V=W.map((j)=>Z.default.basename(j));let z=!1;if(Q.setFileInput)try{await Q.setFileInput(W,'input[type="file"]');z=!0}catch(j){const J=String(j?.message||j);if(!J.includes("Unknown action")&&!J.includes("not supported")&&!J.includes("Not allowed")&&!J.includes("No element found"))throw j}if(!z){const j=W.map((K)=>({name:Z.default.basename(K),mime:P(K),base64:Y.default.readFileSync(K).toString("base64")})),J=requireObjectEvaluateResult(unwrapEvaluateResult(await Q.evaluate(`
|
|
538
|
+
`)),"chatgpt upload preview detection"))return!0}return!1}export async function uploadChatGPTImages(Q,X){const Y=await import("node:fs"),Z=await import("node:path"),$=await prepareChatGPTImagePaths(X);if(!$.ok)return $;const W=$.paths,V=W.map((j)=>Z.default.basename(j));let z=!1;if(Q.setFileInput)try{await Q.setFileInput(W,'input[type="file"]');z=!0}catch(j){const J=String(j?.message||j);if(!J.includes("Unknown action")&&!J.includes("not supported")&&!J.includes("Not allowed")&&!J.includes("File access not granted")&&!J.includes("No element found"))throw j}if(!z){const j=W.map((K)=>({name:Z.default.basename(K),mime:P(K),base64:Y.default.readFileSync(K).toString("base64")})),J=requireObjectEvaluateResult(unwrapEvaluateResult(await Q.evaluate(`
|
|
539
539
|
(() => {
|
|
540
540
|
const files = ${JSON.stringify(j)};
|
|
541
541
|
const input = document.querySelector('input[type="file"]');
|
|
@@ -919,7 +919,7 @@ import{htmlToMarkdown as i}from"@jackwener/opencli/utils";import{ArgumentError a
|
|
|
919
919
|
|
|
920
920
|
return { ok: false };
|
|
921
921
|
})()
|
|
922
|
-
`)),Y=typeof X==="boolean"?{ok:X}:requireObjectEvaluateResult(X,"chatgpt project knowledge dialog open");if(Y.ok){if(Y.nativeClick&&typeof Q.nativeClick==="function")try{await Q.nativeClick(Y.nativeClick.x,Y.nativeClick.y)}catch{}if(Y.nativeClick&&typeof Q.click==="function")try{await Q.click('[role="tab"][id$="-sources"]')}catch{}await Q.wait(1);try{await Q.wait({selector:'[role="dialog"], [data-project-home-sources-surface="true"] input[type="file"]',timeout:5})}catch{}return!0}return!1}export async function uploadChatGPTProjectFiles(Q,X,Y){const Z=parseChatGPTProjectId(X),$=await import("node:fs"),W=await import("node:path"),V=await prepareChatGPTFilePaths(Y);if(!V.ok)return{...V,inputError:!0};const z=V.paths;await navigateToProject(Q,Z);await ensureChatGPTLogin(Q,"ChatGPT project file upload requires a logged-in ChatGPT session.");if(!await openProjectKnowledgeDialog(Q))return{ok:!1,reason:'could not find or click the project "Add files" button'};const j=z.map((B)=>W.default.basename(B));let J=!1;if(Q.setFileInput)for(const B of d)try{await Q.setFileInput(z,B);J=!0;break}catch(H){const D=String(H?.message||H);if(!D.includes("Unknown action")&&!D.includes("not supported")&&!D.includes("Not allowed")&&!D.includes("No element found"))throw H}if(!J){const B=z.map((D)=>({name:W.default.basename(D),mime:
|
|
922
|
+
`)),Y=typeof X==="boolean"?{ok:X}:requireObjectEvaluateResult(X,"chatgpt project knowledge dialog open");if(Y.ok){if(Y.nativeClick&&typeof Q.nativeClick==="function")try{await Q.nativeClick(Y.nativeClick.x,Y.nativeClick.y)}catch{}if(Y.nativeClick&&typeof Q.click==="function")try{await Q.click('[role="tab"][id$="-sources"]')}catch{}await Q.wait(1);try{await Q.wait({selector:'[role="dialog"], [data-project-home-sources-surface="true"] input[type="file"]',timeout:5})}catch{}return!0}return!1}export async function uploadChatGPTProjectFiles(Q,X,Y){const Z=parseChatGPTProjectId(X),$=await import("node:fs"),W=await import("node:path"),V=await prepareChatGPTFilePaths(Y);if(!V.ok)return{...V,inputError:!0};const z=V.paths;await navigateToProject(Q,Z);await ensureChatGPTLogin(Q,"ChatGPT project file upload requires a logged-in ChatGPT session.");if(!await openProjectKnowledgeDialog(Q))return{ok:!1,reason:'could not find or click the project "Add files" button'};const j=z.map((B)=>W.default.basename(B));let J=!1;if(Q.setFileInput)for(const B of d)try{await Q.setFileInput(z,B);J=!0;break}catch(H){const D=String(H?.message||H);if(!D.includes("Unknown action")&&!D.includes("not supported")&&!D.includes("Not allowed")&&!D.includes("File access not granted")&&!D.includes("No element found"))throw H}if(!J){const B=z.map((D)=>({name:W.default.basename(D),mime:i(D),base64:$.default.readFileSync(D).toString("base64")})),H=requireObjectEvaluateResult(unwrapEvaluateResult(await Q.evaluate(`
|
|
923
923
|
(() => {
|
|
924
924
|
const files = ${JSON.stringify(B)};
|
|
925
925
|
|
|
@@ -995,7 +995,7 @@ import{htmlToMarkdown as i}from"@jackwener/opencli/utils";import{ArgumentError a
|
|
|
995
995
|
if (!missing.length) return { ok: true };
|
|
996
996
|
return { ok: false, pending: true, reason: 'uploaded file did not appear in project knowledge: ' + missing.join(', ') };
|
|
997
997
|
})()
|
|
998
|
-
`)),"chatgpt project upload confirmation");if(W.ok===!0)return{ok:!0};Z=String(W.reason||Z);if(!W.pending)return{ok:!1,reason:Z};await Q.wait(0.5)}return{ok:!1,reason:Z}}export async function prepareChatGPTFilePaths(Q){const X=await import("node:fs"),Y=await import("node:path"),Z=Q.map(($)=>Y.default.resolve($));for(const $ of Z){if(!X.default.existsSync($))return{ok:!1,reason:`File not found: ${$}`};const W=X.default.statSync($);if(!W.isFile())return{ok:!1,reason:`Not a file: ${$}`};if(W.size>536870912)return{ok:!1,reason:`File too large (${(W.size/1024/1024).toFixed(1)} MB). Max: 512 MB`}}return{ok:!0,paths:Z}}function
|
|
998
|
+
`)),"chatgpt project upload confirmation");if(W.ok===!0)return{ok:!0};Z=String(W.reason||Z);if(!W.pending)return{ok:!1,reason:Z};await Q.wait(0.5)}return{ok:!1,reason:Z}}export async function prepareChatGPTFilePaths(Q){const X=await import("node:fs"),Y=await import("node:path"),Z=Q.map(($)=>Y.default.resolve($));for(const $ of Z){if(!X.default.existsSync($))return{ok:!1,reason:`File not found: ${$}`};const W=X.default.statSync($);if(!W.isFile())return{ok:!1,reason:`Not a file: ${$}`};if(W.size>536870912)return{ok:!1,reason:`File too large (${(W.size/1024/1024).toFixed(1)} MB). Max: 512 MB`}}return{ok:!0,paths:Z}}function i(Q){const X=String(Q||"").toLowerCase();if(X.endsWith(".pdf"))return"application/pdf";if(X.endsWith(".doc"))return"application/msword";if(X.endsWith(".docx"))return"application/vnd.openxmlformats-officedocument.wordprocessingml.document";if(X.endsWith(".xls"))return"application/vnd.ms-excel";if(X.endsWith(".xlsx"))return"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";if(X.endsWith(".ppt"))return"application/vnd.ms-powerpoint";if(X.endsWith(".pptx"))return"application/vnd.openxmlformats-officedocument.presentationml.presentation";if(X.endsWith(".csv"))return"text/csv";if(X.endsWith(".txt"))return"text/plain";if(X.endsWith(".json"))return"application/json";if(X.endsWith(".xml"))return"application/xml";if(X.endsWith(".html")||X.endsWith(".htm"))return"text/html";if(X.endsWith(".md"))return"text/markdown";if(X.endsWith(".py"))return"text/x-python";if(X.endsWith(".js"))return"text/javascript";if(X.endsWith(".ts"))return"application/typescript";if(X.endsWith(".jsx"))return"text/jsx";if(X.endsWith(".tsx"))return"text/tsx";if(X.endsWith(".png"))return"image/png";if(X.endsWith(".jpg")||X.endsWith(".jpeg"))return"image/jpeg";if(X.endsWith(".gif"))return"image/gif";if(X.endsWith(".webp"))return"image/webp";if(X.endsWith(".svg"))return"image/svg+xml";return"application/octet-stream"}export const __test__={COMPOSER_SELECTORS:x,SEND_BUTTON_SELECTOR:U,SEND_BUTTON_FALLBACK_SELECTORS:L,SEND_BUTTON_LABELS:y,CLOSE_SIDEBAR_LABELS:A,buildComposerLocatorScript:v,isSameChatGPTConversation:N,parseChatGPTConversationId,parseChatGPTProjectId,imageMimeFromPath:P,mimeFromFilePath:i,PROJECT_LINK_SELECTOR:c};export async function getChatGPTImageAssets(Q,X){const Y=JSON.stringify(X);return requireArrayEvaluateResult(unwrapEvaluateResult(await Q.evaluate(`
|
|
999
999
|
(async (targetUrls) => {
|
|
1000
1000
|
const blobToDataUrl = (blob) => new Promise((resolve, reject) => {
|
|
1001
1001
|
const reader = new FileReader();
|
package/clis/claude/utils.js
CHANGED
|
@@ -156,7 +156,7 @@ import{ArgumentError as Y,AuthRequiredError as F,CommandExecutionError as W}from
|
|
|
156
156
|
}
|
|
157
157
|
inp.dispatchEvent(new Event('change', { bubbles: true }));
|
|
158
158
|
return { ok: true, via: 'native' };
|
|
159
|
-
})()`);if(!j?.ok)return j;Z=!0}catch(j){const q=String(j?.message||j);if(!q.includes("Unknown action")&&!q.includes("not supported")&&!q.includes("Not allowed"))throw j}if(!Z){const q=K.default.readFileSync(Q).toString("base64"),J=await z.evaluate(`(async () => {
|
|
159
|
+
})()`);if(!j?.ok)return j;Z=!0}catch(j){const q=String(j?.message||j);if(!q.includes("Unknown action")&&!q.includes("not supported")&&!q.includes("Not allowed")&&!q.includes("File access not granted"))throw j}if(!Z){const q=K.default.readFileSync(Q).toString("base64"),J=await z.evaluate(`(async () => {
|
|
160
160
|
var binary = atob('${q}');
|
|
161
161
|
var bytes = new Uint8Array(binary.length);
|
|
162
162
|
for (var i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
package/clis/deepseek/utils.js
CHANGED
|
@@ -55,7 +55,7 @@ import{ArgumentError as F}from"@jackwener/opencli/errors";export const DEEPSEEK_
|
|
|
55
55
|
return { ok: true, method: 'enter' };
|
|
56
56
|
})()`)}export async function getBubbleCount(q){return await q.evaluate(`(() => {
|
|
57
57
|
return document.querySelectorAll('${MESSAGE_SELECTOR}').length;
|
|
58
|
-
})()`)||0}export function parseThinkingResponse(q){if(!q)return null;const z=q.match(/^(Thought for ([\d.]+) seconds?|已思考(用时 ([\d.]+) 秒))\s*/);if(!z)return{response:q,thinking:null,thinking_time:null};const B=z[2]||z[3];return{response:"",thinking:q.slice(z[0].length).trim(),thinking_time:B}}export async function waitForResponse(q,z,B,Q,Z=!1){const Y=Date.now();let $="",
|
|
58
|
+
})()`)||0}export function parseThinkingResponse(q){if(!q)return null;const z=q.match(/^(Thought for ([\d.]+) seconds?|已思考(用时 ([\d.]+) 秒))\s*/);if(!z)return{response:q,thinking:null,thinking_time:null};const B=z[2]||z[3];return{response:"",thinking:q.slice(z[0].length).trim(),thinking_time:B}}export async function waitForResponse(q,z,B,Q,Z=!1){const Y=Date.now();let $="",K=0;while(Date.now()-Y<Q){await q.wait(3);let W;try{W=await q.evaluate(`(() => {
|
|
59
59
|
const bubbles = document.querySelectorAll('${MESSAGE_SELECTOR}');
|
|
60
60
|
const texts = Array.from(bubbles).map(b => (b.innerText || '').trim()).filter(Boolean);
|
|
61
61
|
var last = texts[texts.length - 1] || '';
|
|
@@ -103,7 +103,7 @@ import{ArgumentError as F}from"@jackwener/opencli/errors";export const DEEPSEEK_
|
|
|
103
103
|
answerText: answerEl ? (answerEl.innerText || '').trim() : null,
|
|
104
104
|
thinkTime: thinkTime,
|
|
105
105
|
};
|
|
106
|
-
})()`)}catch{continue}if(!W)continue;const j=W.last;if(j&&W.count>z&&j!==B.trim()){if(j===$){
|
|
106
|
+
})()`)}catch{continue}if(!W)continue;const j=W.last;if(j&&W.count>z&&j!==B.trim()){if(j===$){K++;if(K>=3){if(Z){if(W.thinkText!=null||W.answerText!=null)return{thinking:W.thinkText||"",response:W.answerText||"",thinking_time:W.thinkTime||null};return parseThinkingResponse(j)}return j}}else K=0;$=j}}if(Z&&$)return parseThinkingResponse($);return $||null}export async function getVisibleMessages(q){const z=await q.evaluate(`(() => {
|
|
107
107
|
const msgs = document.querySelectorAll('${MESSAGE_SELECTOR}');
|
|
108
108
|
return Array.from(msgs).map(m => {
|
|
109
109
|
// User messages carry an extra hash-class alongside ds-message
|
|
@@ -164,17 +164,17 @@ import{ArgumentError as F}from"@jackwener/opencli/errors";export const DEEPSEEK_
|
|
|
164
164
|
while (c && !c.querySelector('div[role="button"]')) c = c.parentElement;
|
|
165
165
|
if (!c) return false;
|
|
166
166
|
return !!c.querySelector('img[src], canvas, video, [style*="background-image"], [class*="preview"], [class*="upload"]');
|
|
167
|
-
})()`))return!0}return!1}export async function sendWithFile(q,z,B){const Q=await import("node:fs"),Z=await import("node:path"),Y=Z.default.resolve(z);if(!Q.default.existsSync(Y))return{ok:!1,reason:`File not found: ${Y}`};const $=Q.default.statSync(Y);if($.size>104857600)return{ok:!1,reason:`File too large (${($.size/1024/1024).toFixed(1)} MB). Max: 100 MB`};const
|
|
167
|
+
})()`))return!0}return!1}export async function sendWithFile(q,z,B){const Q=await import("node:fs"),Z=await import("node:path"),Y=Z.default.resolve(z);if(!Q.default.existsSync(Y))return{ok:!1,reason:`File not found: ${Y}`};const $=Q.default.statSync(Y);if($.size>104857600)return{ok:!1,reason:`File too large (${($.size/1024/1024).toFixed(1)} MB). Max: 100 MB`};const K=Z.default.basename(Y);await q.evaluate(`(() => {
|
|
168
168
|
if (document.querySelectorAll('a[href*="/a/chat/s/"]').length > 0) {
|
|
169
169
|
const btn = document.querySelector('div[tabindex="0"][role="button"]');
|
|
170
170
|
if (btn) btn.click();
|
|
171
171
|
}
|
|
172
|
-
})()`);await q.wait(0.5);let W=!1;if(q.setFileInput)try{await q.setFileInput([Y],'input[type="file"]');W=!0}catch(y){const
|
|
173
|
-
var binary = atob('${
|
|
172
|
+
})()`);await q.wait(0.5);let W=!1;if(q.setFileInput)try{await q.setFileInput([Y],'input[type="file"]');W=!0}catch(y){const J=String(y?.message||y);if(!J.includes("Unknown action")&&!J.includes("not supported")&&!J.includes("Not allowed")&&!J.includes("File access not granted"))throw y}if(!W){const J=Q.default.readFileSync(Y).toString("base64"),U=await q.evaluate(`(async () => {
|
|
173
|
+
var binary = atob('${J}');
|
|
174
174
|
var bytes = new Uint8Array(binary.length);
|
|
175
175
|
for (var i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
|
176
176
|
|
|
177
|
-
var file = new File([bytes], ${JSON.stringify(
|
|
177
|
+
var file = new File([bytes], ${JSON.stringify(K)});
|
|
178
178
|
var dt = new DataTransfer();
|
|
179
179
|
dt.items.add(file);
|
|
180
180
|
|
|
@@ -190,7 +190,7 @@ import{ArgumentError as F}from"@jackwener/opencli/errors";export const DEEPSEEK_
|
|
|
190
190
|
// Use inp.files, not dt.files; assignment transfers ownership
|
|
191
191
|
inp[propsKey].onChange({ target: { files: inp.files } });
|
|
192
192
|
return { ok: true };
|
|
193
|
-
})()`);if(U&&!U.ok)return U}if(!await G(q,
|
|
193
|
+
})()`);if(U&&!U.ok)return U}if(!await G(q,K))return{ok:!1,reason:"file preview did not appear"};let V=!1;for(let y=0;y<15;y++){if(await q.evaluate(`(() => {
|
|
194
194
|
var box = document.querySelector('${TEXTAREA_SELECTOR}');
|
|
195
195
|
if (!box) return false;
|
|
196
196
|
var c = box.parentElement;
|
package/clis/douyin/draft.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import*as
|
|
1
|
+
import*as W from"node:fs";import*as J from"node:path";import{cli as M,Strategy as f}from"@jackwener/opencli/registry";import{ArgumentError as N,CommandExecutionError as $}from"@jackwener/opencli/errors";import{setInputFiles as U}from"../_shared/file-inject.js";const j={public:"公开",friends:"好友可见",private:"仅自己可见"},R="https://creator.douyin.com/creator-micro/content/upload",D=120,F=20,O=20,y=200;async function B(q){await q.evaluate(`() => {
|
|
2
2
|
const targets = ['我知道了', '知道了', '关闭'];
|
|
3
3
|
for (const text of targets) {
|
|
4
4
|
const btn = Array.from(document.querySelectorAll('button,[role="button"]'))
|
|
5
5
|
.find((el) => (el.textContent || '').trim() === text);
|
|
6
6
|
if (btn instanceof HTMLElement) btn.click();
|
|
7
7
|
}
|
|
8
|
-
}`)}async function
|
|
8
|
+
}`)}async function b(q){let G={href:"",ready:!1,bodyText:""};for(let z=0;z<D;z+=1){G=await q.evaluate(`() => ({
|
|
9
9
|
href: location.href,
|
|
10
10
|
ready: !!Array.from(document.querySelectorAll('input')).find(
|
|
11
11
|
(el) => (el.placeholder || '').includes('填写作品标题')
|
|
@@ -13,7 +13,7 @@ import*as Y from"node:fs";import*as J from"node:path";import{cli as V,Strategy a
|
|
|
13
13
|
(el) => (el.textContent || '').includes('暂存离开')
|
|
14
14
|
),
|
|
15
15
|
bodyText: document.body?.innerText || ''
|
|
16
|
-
})`);if(G.ready)return;await q.wait({time:0.5})}throw new $("等待抖音草稿编辑页超时",`当前页面: ${G.href||"unknown"}`)}async function
|
|
16
|
+
})`);if(G.ready)return;await q.wait({time:0.5})}throw new $("等待抖音草稿编辑页超时",`当前页面: ${G.href||"unknown"}`)}async function x(q,G){for(let z=0;z<y;z+=1){const H=await q.evaluate(`() => {
|
|
17
17
|
const text = document.body?.innerText || '';
|
|
18
18
|
const uploading = /上传中|上传\\s*\\d+%|视频上传中|处理中/.test(text);
|
|
19
19
|
const saveBtn = Array.from(document.querySelectorAll('button')).find(
|
|
@@ -71,7 +71,7 @@ import*as Y from"node:fs";import*as J from"node:path";import{cli as V,Strategy a
|
|
|
71
71
|
if (!(visibility instanceof HTMLElement)) return false;
|
|
72
72
|
visibility.click();
|
|
73
73
|
return true;
|
|
74
|
-
}`);if(Q)break;await q.wait({time:0.5})}if(!Q)throw new $("填写抖音草稿表单失败: visibility-missing")}async function
|
|
74
|
+
}`);if(Q)break;await q.wait({time:0.5})}if(!Q)throw new $("填写抖音草稿表单失败: visibility-missing")}async function h(q){let G="cover-input-missing";const z=await q.evaluate(`() => Array.from(document.querySelectorAll('input[type="file"]')).length`);for(let H=0;H<F;H+=1){const Q=await q.evaluate(`() => {
|
|
75
75
|
const coverLabel = Array.from(document.querySelectorAll('label')).find(
|
|
76
76
|
(el) => (el.textContent || '').includes('上传新封面')
|
|
77
77
|
);
|
|
@@ -110,7 +110,7 @@ import*as Y from"node:fs";import*as J from"node:path";import{cli as V,Strategy a
|
|
|
110
110
|
root = root.parentElement;
|
|
111
111
|
}
|
|
112
112
|
return '';
|
|
113
|
-
}`}async function
|
|
113
|
+
}`}async function L(q){return await q.evaluate(buildCoverCheckPanelTextJs())||""}async function k(q){let G="",z=!1;for(let H=0;H<O;H+=1){const Q=await L(q),X=Q.includes("检测中"),Z=Q.includes("重新检测")||Q.includes("横/竖双封面缺失");if(X)z=!0;if(z&&Z&&!X)return;G=Q;await q.wait({time:0.5})}throw new $("等待抖音封面处理完成超时",G||"unknown")}async function S(q){const G=60;let z=null;for(let H=0;H<G;H+=1){z=await q.evaluate(`() => {
|
|
114
114
|
const extractCreationId = () => {
|
|
115
115
|
const titleInput = Array.from(document.querySelectorAll('input')).find(
|
|
116
116
|
(el) => (el.placeholder || '').includes('填写作品标题')
|
|
@@ -163,4 +163,4 @@ import*as Y from"node:fs";import*as J from"node:path";import{cli as V,Strategy a
|
|
|
163
163
|
}`);if(z?.ok)break;await q.wait({time:0.5})}if(!z?.ok)throw new $(`点击草稿按钮失败: ${z?.reason||"unknown"}`);if(!z.creationId)throw new $("点击草稿按钮失败: creation-id-missing");return{text:z.text||"暂存离开",creationId:z.creationId}}async function _(q,G){let z={href:"",bodyText:""};for(let H=0;H<40;H+=1){z=await q.evaluate(`() => ({
|
|
164
164
|
href: location.href,
|
|
165
165
|
bodyText: document.body?.innerText || ''
|
|
166
|
-
})`);const Q=z.href||"",X=z.bodyText||"",Z=Q.includes("/creator-micro/content/upload")&&/继续编辑/.test(X),
|
|
166
|
+
})`);const Q=z.href||"",X=z.bodyText||"",Z=Q.includes("/creator-micro/content/upload")&&/继续编辑/.test(X),K=/草稿保存成功|已保存到草稿|存草稿成功|保存成功/.test(X),Y=/\/creator-micro\/content\/(manage|drafts|works)/.test(Q)||/创作中心/.test(X)&&!Q.includes("/content/upload");if(Z||K||Y)return G;await q.wait({time:1})}throw new $("未检测到抖音草稿保存确认",`当前页面: ${z.href||"unknown"}`)}M({site:"douyin",name:"draft",access:"write",description:"上传视频并保存为草稿",domain:"creator.douyin.com",strategy:f.COOKIE,navigateBefore:!1,defaultWindowMode:"foreground",args:[{name:"video",required:!0,positional:!0,help:"视频文件路径"},{name:"title",required:!0,help:"视频标题(≤30字)"},{name:"caption",default:"",help:"正文内容(≤1000字,支持 #话题)"},{name:"cover",default:"",help:"封面图片路径"},{name:"visibility",default:"public",choices:["public","friends","private"]},{name:"timeout",type:"int",default:180,help:"命令超时(秒),视频上传/转码慢时可调大"}],columns:["status","draft_id"],func:async(q,G)=>{const z=J.resolve(G.video);if(!W.existsSync(z))throw new N(`视频文件不存在: ${z}`);const H=J.extname(z).toLowerCase();if(![".mp4",".mov",".avi",".webm"].includes(H))throw new N(`不支持的视频格式: ${H}(支持 mp4/mov/avi/webm)`);const Q=G.title;if(Q.length>30)throw new N("标题不能超过 30 字");const X=G.caption||"";if(X.length>1000)throw new N("正文不能超过 1000 字");const Z=G.cover;if(Z){if(!W.existsSync(J.resolve(Z)))throw new N(`封面文件不存在: ${J.resolve(Z)}`)}const K=j[G.visibility]??j.public;await q.goto(R);await q.wait({selector:'input[type="file"]',timeout:20});await B(q);await U(q,'input[type="file"]',[z]);await b(q);await x(q,K);await B(q);if(Z){const V=await h(q);await U(q,V,[J.resolve(Z)]);await k(q)}await w(q,{title:Q,caption:X,visibilityLabel:K});await q.wait({time:1});const Y=await S(q);return[{status:"✅ 草稿已保存,可在创作中心继续编辑",draft_id:await _(q,Y.creationId)}]}});
|
|
@@ -1,39 +1,18 @@
|
|
|
1
|
-
import*as
|
|
2
|
-
(async () => {
|
|
3
|
-
const images = ${JSON.stringify(V)};
|
|
4
|
-
const input = document.querySelector(${JSON.stringify(D)});
|
|
5
|
-
if (!input) return { ok: false, error: '页面上找不到图片输入框' };
|
|
6
|
-
const dt = new DataTransfer();
|
|
7
|
-
for (const img of images) {
|
|
8
|
-
try {
|
|
9
|
-
const binary = atob(img.base64);
|
|
10
|
-
const bytes = new Uint8Array(binary.length);
|
|
11
|
-
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
|
12
|
-
dt.items.add(new File([new Blob([bytes], { type: img.mimeType })], img.name, { type: img.mimeType }));
|
|
13
|
-
} catch (e) {
|
|
14
|
-
return { ok: false, error: 'File 构造失败: ' + e.message };
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
Object.defineProperty(input, 'files', { value: dt.files, writable: false });
|
|
18
|
-
input.dispatchEvent(new Event('change', { bubbles: true }));
|
|
19
|
-
input.dispatchEvent(new Event('input', { bubbles: true }));
|
|
20
|
-
return { ok: true };
|
|
21
|
-
})()
|
|
22
|
-
`)}async function p(q,J=20000){const K=500,V=Math.ceil(J/K);for(let z=0;z<V;z++){if(await q.evaluate(`(() => {
|
|
1
|
+
import*as y from"node:fs";import*as N from"node:path";import{cli as h,Strategy as d}from"@jackwener/opencli/registry";import{ArgumentError as Z,CommandExecutionError as B}from"@jackwener/opencli/errors";import{assertLiteralContent as T}from"../_shared/content-guard.js";import{setInputFiles as u}from"../_shared/file-inject.js";import{browserFetch as m}from"./_shared/browser-fetch.js";const c="https://creator.douyin.com/creator-micro/content/upload?default-tab=3",x=35,L=20,w=1000,E=2500,l={".jpg":"image/jpeg",".jpeg":"image/jpeg",".png":"image/png",".bmp":"image/bmp",".webp":"image/webp",".tif":"image/tiff",".tiff":"image/tiff"},M='input[type="file"][accept*="image"]',A=['input[placeholder="添加作品标题"]','input[placeholder*="标题"]'],n='input[placeholder="添加作品标题"]',_='.zone-container[contenteditable="true"]';function o(q){return q.map((J)=>{const z=N.resolve(J);if(!y.existsSync(z))throw new Z(`图片文件不存在: ${z}`);const K=N.extname(z).toLowerCase();if(!l[K])throw new Z(`不支持的图片格式 "${K}",支持: jpg, png, bmp, webp, tif`);return z})}async function i(q,J=15000){const z=500,K=Math.max(1,Math.ceil(J/z));for(let Q=0;Q<K;Q++){if(await q.evaluate(`(() => !!document.querySelector(${JSON.stringify(M)}))()`))return!0;if(Q<K-1)await q.wait({time:z/1000})}return!1}async function p(q,J){if(!await i(q))return{ok:!1,error:"上传页未渲染出图片输入框(等待 15s 超时)"};try{await u(q,M,J);return{ok:!0}}catch(K){return{ok:!1,error:K instanceof Error?K.message:String(K)}}}async function s(q,J=20000){const z=500,K=Math.ceil(J/z);for(let Q=0;Q<K;Q++){if(await q.evaluate(`(() => {
|
|
23
2
|
const onEditor = location.href.includes('/content/post/image');
|
|
24
|
-
const titleReady = ${JSON.stringify(
|
|
3
|
+
const titleReady = ${JSON.stringify(A)}.some((s) => !!document.querySelector(s));
|
|
25
4
|
return onEditor && titleReady;
|
|
26
|
-
})()`))return!0;await q.wait({time:
|
|
5
|
+
})()`))return!0;await q.wait({time:z/1000})}return!1}async function g(q,J){await q.typeText(n,J);await q.wait({time:0.3});const z=await q.evaluate(`
|
|
27
6
|
((selectors) => {
|
|
28
7
|
const el = selectors.map((s) => document.querySelector(s)).find(Boolean);
|
|
29
8
|
return el ? (el.value || '') : '';
|
|
30
|
-
})(${JSON.stringify(
|
|
31
|
-
`);if(
|
|
9
|
+
})(${JSON.stringify(A)})
|
|
10
|
+
`);if(z.trim()!==J){await q.screenshot({path:"/tmp/douyin_image_title_debug.png"});throw new B(`填写标题失败,期望 "${J}",实际 "${z}"。截图: /tmp/douyin_image_title_debug.png`)}}async function r(q,J){await q.typeText(_,J);await q.wait({time:0.5});const z=await q.evaluate(`
|
|
32
11
|
((selector) => {
|
|
33
12
|
const el = Array.from(document.querySelectorAll(selector)).find((n) => n && n.offsetParent !== null);
|
|
34
13
|
return el ? (el.innerText || el.textContent || '').replace(/\\u200b/g, '').trim() : '';
|
|
35
14
|
})(${JSON.stringify(_)})
|
|
36
|
-
`),
|
|
15
|
+
`),K=J.split("#")[0].replace(/\s+/g,"").trim(),Q=z.replace(/\s+/g,"");if(K&&!Q.includes(K.slice(0,Math.min(K.length,20)))){await q.screenshot({path:"/tmp/douyin_image_body_debug.png"});throw new B(`正文写入校验失败,实际读到 "${z.slice(0,80)}"。截图: /tmp/douyin_image_body_debug.png`)}}async function t(q,J){const z=J?["暂存离开"]:["发布"];return await q.evaluate(`
|
|
37
16
|
((labels) => {
|
|
38
17
|
const isVisible = (el) => {
|
|
39
18
|
if (!el || el.offsetParent === null) return false;
|
|
@@ -48,8 +27,8 @@ import*as R from"node:fs";import*as G from"node:path";import{cli as P,Strategy a
|
|
|
48
27
|
if (btn) { btn.click(); return { ok: true, text: label }; }
|
|
49
28
|
}
|
|
50
29
|
return { ok: false, seen: btns.filter(isVisible).map((b) => norm(b.innerText || b.textContent)).filter(Boolean).slice(0, 12) };
|
|
51
|
-
})(${JSON.stringify(
|
|
52
|
-
`)}
|
|
30
|
+
})(${JSON.stringify(z)})
|
|
31
|
+
`)}h({site:"douyin",name:"publish-image",access:"write",description:"发布图文(图集)到抖音(UI 自动化,最多 35 张图;话题以 #词 写进正文)",domain:"creator.douyin.com",strategy:d.COOKIE,browser:!0,navigateBefore:!1,defaultWindowMode:"foreground",args:[{name:"content",required:!1,positional:!0,help:"正文(字面文本,不展开 @文件 引用;长正文用 --file)"},{name:"file",required:!1,help:"从本机文件读取正文(UTF-8),与位置参数 <content> 二选一"},{name:"title",required:!0,help:"作品标题(≤20字)"},{name:"images",required:!0,help:"图片路径,逗号分隔,最多35张 (jpg/png/bmp/webp/tif)"},{name:"topics",required:!1,help:"话题标签,逗号分隔,不含 # 号"},{name:"draft",type:"bool",default:!1,help:"暂存为草稿,不直接发布"}],columns:["status","detail"],func:async(q,J)=>{if(!q)throw Error("Browser page required");const z=String(J.title??"").trim(),K=J.file?String(J.file).trim():"";let Q=String(J.content??"").trim();if(K){if(Q)throw new Z("正文位置参数和 --file 只能二选一");const V=N.resolve(K);if(!y.existsSync(V))throw new Z(`--file 指向的文件不存在: ${V}`);Q=y.readFileSync(V,"utf-8").trim();if(!Q)throw new Z(`--file 指向的文件内容为空: ${V}`)}const G=String(J.images??"").split(",").map((V)=>V.trim()).filter(Boolean),j=J.topics?String(J.topics).split(",").map((V)=>V.trim().replace(/^#/,"")).filter(Boolean):[],$=Boolean(J.draft);if(!z)throw new Z("--title 必填");if(z.length>L)throw new Z(`标题 ${z.length} 字,需 ≤ ${L} 字`);if(!Q)throw new Z("缺少正文:给位置参数 <content>,或用 --file <本机文件路径>");T(Q,{fileFlag:"--file"});if(Q.length>w)throw new Z(`正文 ${Q.length} 字,需 ≤ ${w} 字`);if(G.length===0)throw new Z("至少提供 1 张图片(--images)");if(G.length>x)throw new Z(`图片过多: ${G.length}(最多 ${x} 张)`);const D=o(G),C=j.length?`${Q} ${j.map((V)=>`#${V}`).join(" ")} `:Q;await q.goto(c);let W="";for(let V=0;V<30;V++){await q.wait({time:0.5});W=await q.evaluate("() => location.href");if(W.includes("creator.douyin.com/creator-micro/content"))break}if(!W.includes("creator.douyin.com")){await q.screenshot({path:"/tmp/douyin_image_redirect_debug.png"});throw new B(`被重定向离开创作者中心(当前 ${W})——登录态可能已过期。`+"用 ppcli douyin login 重新登录。截图: /tmp/douyin_image_redirect_debug.png")}const O=await p(q,D);if(!O.ok){await q.screenshot({path:"/tmp/douyin_image_upload_debug.png"});throw new B(`图片上传失败: ${O.error??"unknown"}。截图: /tmp/douyin_image_upload_debug.png`)}if(!await s(q)){await q.screenshot({path:"/tmp/douyin_image_form_debug.png"});throw new B("上传后编辑页未就绪(标题输入框未出现),页面结构可能变化。截图: /tmp/douyin_image_form_debug.png")}await q.wait({time:E/1000});await g(q,z);await q.wait({time:0.4});await r(q,C);await q.wait({time:0.6});const I=await t(q,$);if(!I?.ok){await q.screenshot({path:"/tmp/douyin_image_submit_debug.png"});throw new B(`找不到「${$?"暂存离开":"发布"}」按钮。可见按钮: ${(I?.seen||[]).join(" | ")||"无"}。截图: /tmp/douyin_image_submit_debug.png`)}const P=$?["暂存成功","保存成功","存入草稿"]:["发布成功","提交成功"];let Y=W,X=!1,F="";for(let V=0;V<24;V++){await q.wait({time:0.5});const H=await q.evaluate(`
|
|
53
32
|
((markers) => {
|
|
54
33
|
const url = location.href;
|
|
55
34
|
let toast = '';
|
|
@@ -60,5 +39,5 @@ import*as R from"node:fs";import*as G from"node:path";import{cli as P,Strategy a
|
|
|
60
39
|
if (text && text.length <= 40 && markers.some((m) => text.includes(m))) { toast = text; break; }
|
|
61
40
|
}
|
|
62
41
|
return { url, toast };
|
|
63
|
-
})(${JSON.stringify(
|
|
64
|
-
`);
|
|
42
|
+
})(${JSON.stringify(P)})
|
|
43
|
+
`);Y=H?.url??Y;if(H?.toast){F=H.toast;break}if(Y.includes("/content/manage")||$&&Y.includes("/content/draft")){X=!0;break}}let R=null;if(!X&&!F&&Y.includes("creator.douyin.com"))try{const H=await m(q,"GET",$?"https://creator.douyin.com/web/api/media/aweme/draft/?aid=1128":"https://creator.douyin.com/janus/douyin/creator/pc/work_list?page_size=20&page_num=1&status=0"),b=H.aweme_list??H.data?.work_list??[],U=Q.replace(/\s+/g,"").slice(0,20),f=Date.now()/1000;R=b.find((v)=>{const S=String(v.desc??v.title??"").replace(/\s+/g,""),k=Number(v.create_time??0);return k>0&&f-k<900&&(S.includes(z.replace(/\s+/g,""))||U&&S.includes(U))})??null}catch{}if(!X&&!F&&!R){await q.screenshot({path:"/tmp/douyin_image_verify_debug.png"});throw new B(`${$?"暂存":"发布"}结果无法确认:未跳转作品管理页、未见成功提示,`+`后台${$?"草稿箱":"作品列表"}回查也没找到本次内容——大概率没发出去,可以安全重试。`+`当前 URL: ${Y}。截图: /tmp/douyin_image_verify_debug.png`)}return[{status:`✅ ${$?"暂存成功":"发布成功"}`,detail:[`"${z}"`,`${D.length}张图片`,j.length?`话题: ${j.map((V)=>`#${V}`).join(" ")}`:"",R?.aweme_id?`aweme_id: ${R.aweme_id}(后台回查坐实)`:"",X?Y:F].filter(Boolean).join(" · ")}]}});
|
package/clis/facebook/feed.js
CHANGED
|
@@ -1,8 +1,27 @@
|
|
|
1
|
-
import{ArgumentError as
|
|
2
|
-
const limit = ${
|
|
1
|
+
import{ArgumentError as K,AuthRequiredError as P,CommandExecutionError as Q,EmptyResultError as U}from"@jackwener/opencli/errors";import{cli as h,Strategy as j}from"@jackwener/opencli/registry";const T="https://www.facebook.com/",V=50;function W(f){const J=Number(f);if(!Number.isInteger(J)||J<1||J>V)throw new K(`facebook feed --limit must be an integer between 1 and ${V}`);return J}function Y(f){if(f&&typeof f==="object"&&"data"in f)return f.data;return f}function Z(f){return`(() => {
|
|
2
|
+
const limit = ${f};
|
|
3
3
|
|
|
4
4
|
function clean(value) {
|
|
5
|
-
|
|
5
|
+
// Strip zero-width / bidi control chars first: Facebook injects them into
|
|
6
|
+
// decoy nodes to poison scrapers. See issue #2089.
|
|
7
|
+
return String(value || '')
|
|
8
|
+
.replace(/[\\u200b-\\u200f\\u202a-\\u202e\\u2060\\ufeff]/g, '')
|
|
9
|
+
.replace(/\\s+/g, ' ')
|
|
10
|
+
.trim();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// FB anti-scrape decoys: long spaceless digit runs, spaced single-char
|
|
14
|
+
// strings ("a b c d e"), and hidden-domain .com spam. Real author/content
|
|
15
|
+
// text never looks like this.
|
|
16
|
+
function isDecoyText(text) {
|
|
17
|
+
if (!text) return true;
|
|
18
|
+
if (/^\\d{8,}$/.test(text)) return true;
|
|
19
|
+
if (/^(?:\\S ){4,}\\S$/.test(text) && text.replace(/\\s/g, '').length <= 12) return true;
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function isReelsOrCarouselChrome(text) {
|
|
24
|
+
return /^(Reels|Reels and short videos|Reels 和短视频|快拍|短视频|People you may know)/i.test(text);
|
|
6
25
|
}
|
|
7
26
|
|
|
8
27
|
function textOf(el) {
|
|
@@ -94,6 +113,8 @@ import{ArgumentError as m,AuthRequiredError as h,CommandExecutionError as s,Empt
|
|
|
94
113
|
&& !isActionText(text)
|
|
95
114
|
&& !isMetricText(text)
|
|
96
115
|
&& !isTimestampText(text)
|
|
116
|
+
&& !isDecoyText(text)
|
|
117
|
+
&& !/^[\\d\\s.,-]+$/.test(text) // reject all-digit decoy names, but keep "Class of 2024" (#2089)
|
|
97
118
|
&& !/\\/groups\\/|\\/watch\\/|\\/reel\\/|\\/events\\/|\\/friends\\//i.test(href)) {
|
|
98
119
|
return text;
|
|
99
120
|
}
|
|
@@ -107,6 +128,7 @@ import{ArgumentError as m,AuthRequiredError as h,CommandExecutionError as s,Empt
|
|
|
107
128
|
if (text.length <= 10) return false;
|
|
108
129
|
if (isSuggestionOrChrome(text) || isSponsored(text)) return false;
|
|
109
130
|
if (isActionText(text) || isMetricText(text) || isTimestampText(text)) return false;
|
|
131
|
+
if (isDecoyText(text) || isReelsOrCarouselChrome(text)) return false;
|
|
110
132
|
if (/^(See more|查看更多|更多)$/i.test(text)) return false;
|
|
111
133
|
return true;
|
|
112
134
|
});
|
|
@@ -148,6 +170,47 @@ import{ArgumentError as m,AuthRequiredError as h,CommandExecutionError as s,Empt
|
|
|
148
170
|
.filter((el) => textOf(el).length > 30);
|
|
149
171
|
}
|
|
150
172
|
|
|
173
|
+
// Modern Facebook no longer wraps posts in [role="article"] nor exposes the
|
|
174
|
+
// Like/Comment/Share aria-labels the fallback keyed on. Every post still
|
|
175
|
+
// carries one "Actions for this post" control (the ⋯ menu). Anchor on it and
|
|
176
|
+
// walk up to the HIGHEST ancestor that still contains exactly one such
|
|
177
|
+
// control — that bounds the node to a single post. See issue #2089.
|
|
178
|
+
function actionMenuAnchors() {
|
|
179
|
+
// Post menus only — NOT "Actions for this comment", so the anchor set,
|
|
180
|
+
// countMenus, and loadFeedPosts all key on the same thing (#2089).
|
|
181
|
+
return Array.from(document.querySelectorAll('[aria-label]')).filter((el) =>
|
|
182
|
+
/^(Actions for this post|此帖子的操作|针对此帖子的操作|贴文的操作)$/i.test(labelOf(el)));
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function actionAnchoredContainers() {
|
|
186
|
+
const menus = actionMenuAnchors();
|
|
187
|
+
const seen = new WeakSet();
|
|
188
|
+
const containers = [];
|
|
189
|
+
const countMenus = (node) => {
|
|
190
|
+
let n = 0;
|
|
191
|
+
for (const el of node.querySelectorAll('[aria-label]')) {
|
|
192
|
+
if (/^(Actions for this post|此帖子的操作|针对此帖子的操作|贴文的操作)$/i.test(labelOf(el))) n += 1;
|
|
193
|
+
}
|
|
194
|
+
return n;
|
|
195
|
+
};
|
|
196
|
+
const LANDMARK_ROLES = new Set(['main', 'feed', 'banner', 'navigation', 'complementary', 'contentinfo', 'region']);
|
|
197
|
+
for (const menu of menus) {
|
|
198
|
+
let node = menu.parentElement;
|
|
199
|
+
let best = null;
|
|
200
|
+
for (let depth = 0; depth < 22 && node && node !== document.body && node !== document.documentElement; depth += 1, node = node.parentElement) {
|
|
201
|
+
// Never let a single-post page climb the container up to a page
|
|
202
|
+
// landmark (role=main/feed/...) and emit the whole region as a post.
|
|
203
|
+
if (LANDMARK_ROLES.has(node.getAttribute('role') || '')) break;
|
|
204
|
+
if (countMenus(node) === 1) best = node; else break;
|
|
205
|
+
}
|
|
206
|
+
if (best && !seen.has(best) && textOf(best).length > 30) {
|
|
207
|
+
seen.add(best);
|
|
208
|
+
containers.push(best);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return containers;
|
|
212
|
+
}
|
|
213
|
+
|
|
151
214
|
function fallbackContainers() {
|
|
152
215
|
const main = document.querySelector('[role="main"]');
|
|
153
216
|
if (!main) return [];
|
|
@@ -188,7 +251,8 @@ import{ArgumentError as m,AuthRequiredError as h,CommandExecutionError as s,Empt
|
|
|
188
251
|
if (isAuthPage()) return { status: 'auth', rows: [], diagnostics: {} };
|
|
189
252
|
|
|
190
253
|
const primary = primaryContainers();
|
|
191
|
-
const
|
|
254
|
+
const actionAnchored = actionAnchoredContainers();
|
|
255
|
+
const combined = dedupe([...primary, ...actionAnchored, ...fallbackContainers()]);
|
|
192
256
|
const rows = [];
|
|
193
257
|
for (const container of combined) {
|
|
194
258
|
const row = extractPost(container, rows.length + 1);
|
|
@@ -202,8 +266,17 @@ import{ArgumentError as m,AuthRequiredError as h,CommandExecutionError as s,Empt
|
|
|
202
266
|
diagnostics: {
|
|
203
267
|
articleCount: document.querySelectorAll('[role="article"]').length,
|
|
204
268
|
primaryCount: primary.length,
|
|
269
|
+
actionMenuCount: actionMenuAnchors().length,
|
|
205
270
|
fallbackActionCount: document.querySelectorAll('[role="main"] [aria-label="Like"], [role="main"] [aria-label="赞"], [role="main"] [aria-label="Comment"], [role="main"] [aria-label="评论"]').length,
|
|
206
271
|
mainTextLength: textOf(document.querySelector('[role="main"]')).length,
|
|
207
272
|
},
|
|
208
273
|
};
|
|
209
|
-
})()`}async function
|
|
274
|
+
})()`}async function X(f,J){const N=`(async () => {
|
|
275
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
276
|
+
window.scrollTo(0, document.body.scrollHeight);
|
|
277
|
+
await sleep(800);
|
|
278
|
+
return document.querySelectorAll('[aria-label]').length
|
|
279
|
+
? document.querySelectorAll('[role="article"]').length
|
|
280
|
+
+ Array.from(document.querySelectorAll('[aria-label]')).filter((el) => /^(Actions for this post|此帖子的操作|针对此帖子的操作|贴文的操作)$/i.test((el.getAttribute('aria-label') || '').trim())).length
|
|
281
|
+
: 0;
|
|
282
|
+
})()`;let z=-1;for(let G=0;G<8;G+=1){let D=0;try{D=Number(Y(await f.evaluate(N)))||0}catch{break}if(D>=J||D===z)break;z=D}}async function $(f,J){const N=W(J.limit??10);try{await f.goto(T,{settleMs:4000})}catch(D){throw new Q(`Failed to navigate to facebook feed: ${D instanceof Error?D.message:D}`,"Check that facebook.com is reachable and the browser extension is connected.")}await X(f,N);let z;try{z=Y(await f.evaluate(Z(N)))}catch(D){throw new Q(`Failed to read facebook feed: ${D instanceof Error?D.message:D}`,"Facebook may not have rendered or the feed markup may have changed.")}if(!z||typeof z!=="object"||!Array.isArray(z.rows))throw new Q("facebook feed returned malformed extraction payload");if(z.status==="auth")throw new P("www.facebook.com","Open Chrome and log in to Facebook before retrying.");if(z.rows.length>0)return z.rows;if(z.status==="empty")throw new U("facebook feed","Facebook did not show any feed posts for this account.");const G=z.diagnostics||{};if(G.articleCount||G.actionMenuCount||G.fallbackActionCount||G.mainTextLength>200)throw new Q("facebook feed page rendered but no feed rows could be extracted",`Diagnostics: articles=${G.articleCount||0}, actions=${G.fallbackActionCount||0}, mainTextLength=${G.mainTextLength||0}.`);throw new U("facebook feed","No Facebook feed content was visible in the current browser session.")}const H={site:"facebook",name:"feed",access:"read",description:"Get your Facebook news feed",domain:"www.facebook.com",strategy:j.COOKIE,browser:!0,navigateBefore:!1,args:[{name:"limit",type:"int",default:10,help:"Number of posts"}],columns:["index","author","content","likes","comments","shares"],func:$};h(H);export const __test__={buildFeedExtractScript:Z,command:H,getFacebookFeed:$,requireLimit:W};
|
package/clis/facebook/post.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import*as
|
|
1
|
+
import*as y from"node:fs";import*as f from"node:path";import{ArgumentError as q,AuthRequiredError as L,CommandExecutionError as W}from"@jackwener/opencli/errors";import{cli as N,Strategy as A}from"@jackwener/opencli/registry";import{assertLiteralContent as j}from"../_shared/content-guard.js";import{setInputFiles as _}from"../_shared/file-inject.js";const D='[data-lexical-editor="true"][contenteditable="true"]',$='[data-pp-fb-composer="1"]',M='[data-pp-fb-trigger="1"]',U='[data-pp-fb-post="1"]',T="https://www.facebook.com/",I=new Set([".jpg",".jpeg",".png",".gif",".webp"]);function k(z){const K=String(z??"").trim();if(!K)return T;if(/^https?:\/\//i.test(K)){let J;try{J=new URL(K)}catch{throw new q(`Invalid target URL: ${K}`)}if(J.hostname!=="facebook.com"&&!J.hostname.endsWith(".facebook.com"))throw new q("target URL must be on facebook.com.");return J.href}const Q=K.replace(/^@/,"").replace(/^\/+|\/+$/g,"");if(!/^[A-Za-z0-9.\-]+$/.test(Q))throw new q(`Invalid target "${z}" — pass a facebook.com URL or a bare profile/Page vanity.`);return`https://www.facebook.com/${Q}`}function w(z){return String(z).split(",").map((Q)=>Q.trim()).filter(Boolean).map((Q)=>{const J=f.resolve(Q),Y=f.extname(J).toLowerCase();if(!I.has(Y))throw new W(`Unsupported image format "${Y}". Supported: jpg, png, gif, webp`);const Z=y.statSync(J,{throwIfNoEntry:!1});if(!Z||!Z.isFile())throw new W(`Not a valid file: ${J}`);return J})}function R(){return`(() => {
|
|
2
2
|
const path = location.pathname || '';
|
|
3
3
|
if (/^\\/(login|checkpoint)(\\/|$|\\.php)/.test(path)) return 'login';
|
|
4
4
|
// Prefer an already-open editor (dialog first, so we don't grab a
|
|
5
5
|
// background feed publisher when the modal is up).
|
|
6
6
|
const dialog = document.querySelector('[role="dialog"]');
|
|
7
7
|
const scope = dialog || document;
|
|
8
|
-
const ed = scope.querySelector('${
|
|
8
|
+
const ed = scope.querySelector('${D}') || document.querySelector('${D}');
|
|
9
9
|
if (ed) {
|
|
10
10
|
ed.setAttribute('data-pp-fb-composer', '1');
|
|
11
11
|
ed.scrollIntoView({ block: 'center' });
|
|
@@ -33,7 +33,7 @@ import*as v from"node:fs";import*as H from"node:path";import{ArgumentError as V,
|
|
|
33
33
|
return 'wait';
|
|
34
34
|
})()`}function S(){return`(() => {
|
|
35
35
|
const dialog = document.querySelector('[role="dialog"]') || document;
|
|
36
|
-
const ed = dialog.querySelector('${
|
|
36
|
+
const ed = dialog.querySelector('${$}');
|
|
37
37
|
const btns = [...dialog.querySelectorAll('[role="button"][tabindex]')].filter((b) => {
|
|
38
38
|
if (ed && (b === ed || b.contains(ed) || ed.contains(b))) return false;
|
|
39
39
|
if (b.closest('[role="toolbar"]')) return false;
|
|
@@ -46,43 +46,16 @@ import*as v from"node:fs";import*as H from"node:path";import{ArgumentError as V,
|
|
|
46
46
|
submit.setAttribute('data-pp-fb-post', '1');
|
|
47
47
|
submit.scrollIntoView({ block: 'center' });
|
|
48
48
|
return { ok: true };
|
|
49
|
-
})()`}function
|
|
49
|
+
})()`}function b(z){return`(() => {
|
|
50
50
|
const collapse = (s) => String(s || '').replace(/\\s+/g, ' ').trim();
|
|
51
|
-
const ed = document.querySelector('${
|
|
51
|
+
const ed = document.querySelector('${$}');
|
|
52
52
|
if (!ed) return { ok: true };
|
|
53
53
|
const remaining = collapse(ed.textContent || '');
|
|
54
54
|
if (remaining.length === 0) return { ok: true };
|
|
55
55
|
if (remaining === collapse(${JSON.stringify(z)})) return { ok: false, remaining };
|
|
56
56
|
return { ok: true };
|
|
57
|
-
})()`}const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
const dt = new DataTransfer();
|
|
61
|
-
for (const file of ${JSON.stringify(Q)}) {
|
|
62
|
-
const bin = atob(file.base64);
|
|
63
|
-
const bytes = new Uint8Array(bin.length);
|
|
64
|
-
for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
|
|
65
|
-
dt.items.add(new File([bytes], file.name, { type: file.mime }));
|
|
66
|
-
}
|
|
67
|
-
let assigned = false;
|
|
68
|
-
try {
|
|
69
|
-
Object.defineProperty(input, 'files', { value: dt.files, writable: false, configurable: true });
|
|
70
|
-
assigned = input.files && input.files.length >= ${JSON.stringify(J.length)};
|
|
71
|
-
} catch (e) {
|
|
72
|
-
try {
|
|
73
|
-
const setter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'files');
|
|
74
|
-
if (setter && setter.set) {
|
|
75
|
-
setter.set.call(input, dt.files);
|
|
76
|
-
assigned = input.files && input.files.length >= ${JSON.stringify(J.length)};
|
|
77
|
-
}
|
|
78
|
-
} catch (e2) { /* ignore */ }
|
|
79
|
-
}
|
|
80
|
-
if (!assigned) return { ok: false, error: 'Could not assign files to input' };
|
|
81
|
-
input.dispatchEvent(new Event('change', { bubbles: true }));
|
|
82
|
-
input.dispatchEvent(new Event('input', { bubbles: true }));
|
|
83
|
-
return { ok: true };
|
|
84
|
-
})()`);if(!K?.ok)throw new $(`Image upload failed: ${K?.error??"unknown error"}`)}async function x(z,J){if(z.setFileInput)try{await z.setFileInput(J,D);return}catch{}await C(z,J)}N({site:"facebook",name:"post",access:"write",description:"Publish a new post to your Facebook feed (or a Page you manage). Pass the post text (multi-line allowed); optionally attach local images and target a Page. Published from your logged-in account — write genuine, on-topic content, not filler.",domain:"facebook.com",strategy:L.UI,browser:!0,args:[{name:"text",type:"string",required:!0,positional:!0,help:"Post text (multi-line allowed)"},{name:"images",type:"string",required:!1,help:"Local image paths, comma-separated (jpg/png/gif/webp)"},{name:"target",type:"string",required:!1,help:"Optional Page/profile to post to: a facebook.com URL or a bare vanity. Omit to post to your own feed."}],columns:["status","target","message"],func:async(z,J)=>{if(!z)throw new $("Browser session required for facebook post");const Q=M(J.text,{label:"帖子正文"});if(!Q)throw new V("Post text cannot be empty.");const K=J.images?w(J.images):[],Y=T(J.target);await z.goto(Y,{waitUntil:"none"});let Z=!1;for(let W=0;W<20;W++){const F=await z.evaluate(R());if(F==="login")throw new A("www.facebook.com","Facebook redirected to login/checkpoint — not signed in");if(F==="ready"){Z=!0;break}if(F==="trigger")await z.click(_);await z.wait(1)}if(!Z)throw new $("Post composer not found — the feed publisher may not have loaded, or you may lack permission to post here.");await z.click(B);await z.wait(1);await z.pressKey("Control+A");await z.pressKey("Delete");if(K.length>0){await x(z,K);await z.wait(2)}const q=Q.split(`
|
|
85
|
-
`);for(let W=0;W<q.length;W++){if(W>0)await z.pressKey("Shift+Enter");if(q[W].length)await z.typeText(B,q[W])}if(await z.evaluate(`(() => !!document.querySelector('[role="listbox"][aria-label], ul[role="listbox"]'))()`))await z.pressKey("Escape");const G=(W)=>String(W||"").replace(/\s+/g," ").trim(),X=await z.evaluate(`(() => {
|
|
86
|
-
const ed = document.querySelector('${B}');
|
|
57
|
+
})()`}const C='[role="dialog"] input[type="file"], input[type="file"][accept*="image"]';async function O(z,K){await _(z,C,K)}N({site:"facebook",name:"post",access:"write",description:"Publish a new post to your Facebook feed (or a Page you manage). Pass the post text (multi-line allowed); optionally attach local images and target a Page. Published from your logged-in account — write genuine, on-topic content, not filler.",domain:"facebook.com",strategy:A.UI,browser:!0,args:[{name:"text",type:"string",required:!0,positional:!0,help:"Post text (multi-line allowed)"},{name:"images",type:"string",required:!1,help:"Local image paths, comma-separated (jpg/png/gif/webp)"},{name:"target",type:"string",required:!1,help:"Optional Page/profile to post to: a facebook.com URL or a bare vanity. Omit to post to your own feed."}],columns:["status","target","message"],func:async(z,K)=>{if(!z)throw new W("Browser session required for facebook post");const Q=j(K.text,{label:"帖子正文"});if(!Q)throw new q("Post text cannot be empty.");const J=K.images?w(K.images):[],Y=k(K.target);await z.goto(Y,{waitUntil:"none"});let Z=!1;for(let V=0;V<20;V++){const X=await z.evaluate(R());if(X==="login")throw new L("www.facebook.com","Facebook redirected to login/checkpoint — not signed in");if(X==="ready"){Z=!0;break}if(X==="trigger")await z.click(M);await z.wait(1)}if(!Z)throw new W("Post composer not found — the feed publisher may not have loaded, or you may lack permission to post here.");await z.click($);await z.wait(1);await z.pressKey("Control+A");await z.pressKey("Delete");if(J.length>0){await O(z,J);await z.wait(2)}const B=Q.split(`
|
|
58
|
+
`);for(let V=0;V<B.length;V++){if(V>0)await z.pressKey("Shift+Enter");if(B[V].length)await z.typeText($,B[V])}if(await z.evaluate(`(() => !!document.querySelector('[role="listbox"][aria-label], ul[role="listbox"]'))()`))await z.pressKey("Escape");const F=(V)=>String(V||"").replace(/\s+/g," ").trim(),H=await z.evaluate(`(() => {
|
|
59
|
+
const ed = document.querySelector('${$}');
|
|
87
60
|
return ed ? (ed.textContent || '') : null;
|
|
88
|
-
})()`);if(
|
|
61
|
+
})()`);if(H===null||!F(H).includes(F(Q)))throw new W(`Composed text did not land before publish (got ${JSON.stringify((H||"").slice(0,60))}).`);const G=await z.evaluate(S());if(G?.notfound)throw new W("Post button not found in the composer.");if(G?.disabled)throw new W("Post button is disabled — composer may be empty or the post was rejected.");await z.click(U);await z.wait(2);if(!(await z.evaluate(b(Q)))?.ok)throw new W("Post still in the composer after clicking Post — publish blocked (rate limit, restricted, or no permission). Not confirmed sent; verify before retrying.");const v=J.length>0?` with ${J.length} image(s)`:"";return[{status:"success",target:Y,message:`Post published${v}.`}]}});
|