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.
Files changed (58) hide show
  1. package/cli-manifest.json +442 -1
  2. package/clis/_shared/file-inject.js +41 -0
  3. package/clis/_shared/video-publish.js +4 -4
  4. package/clis/bilibili/upload.js +1 -1
  5. package/clis/chatgpt/utils.js +5 -5
  6. package/clis/claude/utils.js +1 -1
  7. package/clis/deepseek/utils.js +7 -7
  8. package/clis/douyin/draft.js +6 -6
  9. package/clis/douyin/publish-image.js +10 -31
  10. package/clis/facebook/feed.js +78 -5
  11. package/clis/facebook/post.js +9 -36
  12. package/clis/facebook/search.js +83 -22
  13. package/clis/instagram/post.js +19 -66
  14. package/clis/instagram/reel.js +11 -11
  15. package/clis/threads/post.js +15 -34
  16. package/clis/twitter/post.js +15 -42
  17. package/clis/twitter/utils.js +4 -36
  18. package/clis/wechat-channels/publish.js +29 -57
  19. package/clis/weibo/publish.js +8 -8
  20. package/clis/weixin/create-draft.js +7 -7
  21. package/clis/xianyu/collect.js +1 -0
  22. package/clis/xianyu/location.js +1 -0
  23. package/clis/xianyu/manage.js +1 -0
  24. package/clis/xianyu/mtop.js +47 -0
  25. package/clis/xianyu/order.js +1 -0
  26. package/clis/xianyu/publish.js +2 -2
  27. package/clis/xianyu/rate.js +1 -0
  28. package/clis/xianyu/user.js +1 -0
  29. package/clis/xiaohongshu/publish-video.js +9 -27
  30. package/clis/xiaohongshu/publish.js +32 -72
  31. package/dist/src/browser/base-page.d.ts +19 -0
  32. package/dist/src/browser/base-page.js +1 -1
  33. package/dist/src/browser/bridge-readiness.d.ts +1 -1
  34. package/dist/src/browser/daemon-client.d.ts +72 -1
  35. package/dist/src/browser/daemon-client.js +1 -1
  36. package/dist/src/browser/daemon-version.d.ts +1 -1
  37. package/dist/src/browser/errors.d.ts +2 -0
  38. package/dist/src/browser/errors.js +3 -3
  39. package/dist/src/browser/network-interceptor.d.ts +11 -0
  40. package/dist/src/browser/network-interceptor.js +1 -0
  41. package/dist/src/browser/target-resolver.d.ts +1 -0
  42. package/dist/src/browser/target-resolver.js +100 -6
  43. package/dist/src/cli.js +24 -24
  44. package/dist/src/commands/daemon.js +1 -1
  45. package/dist/src/daemon-utils.d.ts +1 -0
  46. package/dist/src/daemon-utils.js +1 -1
  47. package/dist/src/daemon.js +1 -1
  48. package/dist/src/doctor.d.ts +1 -1
  49. package/dist/src/doctor.js +7 -7
  50. package/dist/src/errors.d.ts +8 -0
  51. package/dist/src/errors.js +1 -1
  52. package/dist/src/execution.js +2 -2
  53. package/dist/src/external.js +1 -1
  54. package/dist/src/session-lease.d.ts +135 -0
  55. package/dist/src/session-lease.js +1 -0
  56. package/dist/src/session-lease.test.d.ts +1 -0
  57. package/dist/src/types.d.ts +3 -0
  58. package/package.json +1 -1
@@ -1,4 +1,4 @@
1
- import{htmlToMarkdown as i}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`
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 i(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(`(() => {
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:m(D),base64:$.default.readFileSync(D).toString("base64")})),H=requireObjectEvaluateResult(unwrapEvaluateResult(await Q.evaluate(`
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 m(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:m,PROJECT_LINK_SELECTOR:c};export async function getChatGPTImageAssets(Q,X){const Y=JSON.stringify(X);return requireArrayEvaluateResult(unwrapEvaluateResult(await Q.evaluate(`
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();
@@ -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);
@@ -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 $="",J=0;while(Date.now()-Y<Q){await q.wait(3);let W;try{W=await q.evaluate(`(() => {
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===$){J++;if(J>=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 J=0;$=j}}if(Z&&$)return parseThinkingResponse($);return $||null}export async function getVisibleMessages(q){const z=await q.evaluate(`(() => {
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 J=Z.default.basename(Y);await q.evaluate(`(() => {
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 K=String(y?.message||y);if(!K.includes("Unknown action")&&!K.includes("not supported")&&!K.includes("Not allowed"))throw y}if(!W){const K=Q.default.readFileSync(Y).toString("base64"),U=await q.evaluate(`(async () => {
173
- var binary = atob('${K}');
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(J)});
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,J))return{ok:!1,reason:"file preview did not appear"};let V=!1;for(let y=0;y<15;y++){if(await q.evaluate(`(() => {
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;
@@ -1,11 +1,11 @@
1
- import*as Y from"node:fs";import*as J from"node:path";import{cli as V,Strategy as f}from"@jackwener/opencli/registry";import{ArgumentError as N,CommandExecutionError as $}from"@jackwener/opencli/errors";const W={public:"公开",friends:"好友可见",private:"仅自己可见"},F="https://creator.douyin.com/creator-micro/content/upload",M=120,D=20,R=20,O=200;async function U(q){await q.evaluate(`() => {
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 y(q){let G={href:"",ready:!1,bodyText:""};for(let z=0;z<M;z+=1){G=await q.evaluate(`() => ({
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 b(q,G){for(let z=0;z<O;z+=1){const H=await q.evaluate(`() => {
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 x(q){let G="cover-input-missing";const z=await q.evaluate(`() => Array.from(document.querySelectorAll('input[type="file"]')).length`);for(let H=0;H<D;H+=1){const Q=await q.evaluate(`() => {
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 h(q){return await q.evaluate(buildCoverCheckPanelTextJs())||""}async function L(q){let G="",z=!1;for(let H=0;H<R;H+=1){const Q=await h(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 I(q){const G=60;let z=null;for(let H=0;H<G;H+=1){z=await q.evaluate(`() => {
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),j=/草稿保存成功|已保存到草稿|存草稿成功|保存成功/.test(X),K=/\/creator-micro\/content\/(manage|drafts|works)/.test(Q)||/创作中心/.test(X)&&!Q.includes("/content/upload");if(Z||j||K)return G;await q.wait({time:1})}throw new $("未检测到抖音草稿保存确认",`当前页面: ${z.href||"unknown"}`)}V({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(!Y.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(!Y.existsSync(J.resolve(Z)))throw new N(`封面文件不存在: ${J.resolve(Z)}`)}if(!q.setFileInput)throw new $("当前浏览器适配器不支持文件注入","请使用 Browser Bridge 或支持 setFileInput 的浏览器模式");const j=W[G.visibility]??W.public;await q.goto(F);await q.wait({selector:'input[type="file"]',timeout:20});await U(q);await q.setFileInput([z],'input[type="file"]');await y(q);await b(q,j);await U(q);if(Z){const B=await x(q);await q.setFileInput([J.resolve(Z)],B);await L(q)}await w(q,{title:Q,caption:X,visibilityLabel:j});await q.wait({time:1});const K=await I(q);return[{status:"✅ 草稿已保存,可在创作中心继续编辑",draft_id:await _(q,K.creationId)}]}});
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 R from"node:fs";import*as G from"node:path";import{cli as P,Strategy as h}from"@jackwener/opencli/registry";import{ArgumentError as $,CommandExecutionError as j}from"@jackwener/opencli/errors";import{assertLiteralContent as T}from"../_shared/content-guard.js";import{browserFetch as c}from"./_shared/browser-fetch.js";const m="https://creator.douyin.com/creator-micro/content/upload?default-tab=3",w=35,S=20,M=1000,E=2500,A={".jpg":"image/jpeg",".jpeg":"image/jpeg",".png":"image/png",".bmp":"image/bmp",".webp":"image/webp",".tif":"image/tiff",".tiff":"image/tiff"},D='input[type="file"][accept*="image"]',C=['input[placeholder="添加作品标题"]','input[placeholder*="标题"]'],l='input[placeholder="添加作品标题"]',_='.zone-container[contenteditable="true"]';function n(q){return q.map((J)=>{const K=G.resolve(J);if(!R.existsSync(K))throw new $(`图片文件不存在: ${K}`);const V=G.extname(K).toLowerCase();if(!A[V])throw new $(`不支持的图片格式 "${V}",支持: jpg, png, bmp, webp, tif`);return K})}async function o(q,J=15000){const K=500,V=Math.max(1,Math.ceil(J/K));for(let z=0;z<V;z++){if(await q.evaluate(`(() => !!document.querySelector(${JSON.stringify(D)}))()`))return!0;if(z<V-1)await q.wait({time:K/1000})}return!1}async function i(q,J){if(!await o(q))return{ok:!1,error:"上传页未渲染出图片输入框(等待 15s 超时)"};if(q.setFileInput)try{await q.setFileInput(J,D);return{ok:!0}}catch(z){const Z=z instanceof Error?z.message:String(z);if(!(Z.includes("Unknown action")||Z.includes("not supported")||Z.includes("Not allowed")))return{ok:!1,error:Z}}const V=J.map((z)=>({name:G.basename(z),mimeType:A[G.extname(z).toLowerCase()],base64:R.readFileSync(z).toString("base64")}));return q.evaluate(`
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(C)}.some((s) => !!document.querySelector(s));
3
+ const titleReady = ${JSON.stringify(A)}.some((s) => !!document.querySelector(s));
25
4
  return onEditor && titleReady;
26
- })()`))return!0;await q.wait({time:K/1000})}return!1}async function s(q,J){await q.typeText(l,J);await q.wait({time:0.3});const K=await q.evaluate(`
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(C)})
31
- `);if(K.trim()!==J){await q.screenshot({path:"/tmp/douyin_image_title_debug.png"});throw new j(`填写标题失败,期望 "${J}",实际 "${K}"。截图: /tmp/douyin_image_title_debug.png`)}}async function g(q,J){await q.typeText(_,J);await q.wait({time:0.5});const K=await q.evaluate(`
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
- `),V=J.split("#")[0].replace(/\s+/g,"").trim(),z=K.replace(/\s+/g,"");if(V&&!z.includes(V.slice(0,Math.min(V.length,20)))){await q.screenshot({path:"/tmp/douyin_image_body_debug.png"});throw new j(`正文写入校验失败,实际读到 "${K.slice(0,80)}"。截图: /tmp/douyin_image_body_debug.png`)}}async function r(q,J){const K=J?["暂存离开"]:["发布"];return await q.evaluate(`
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(K)})
52
- `)}P({site:"douyin",name:"publish-image",access:"write",description:"发布图文(图集)到抖音(UI 自动化,最多 35 张图;话题以 #词 写进正文)",domain:"creator.douyin.com",strategy:h.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 K=String(J.title??"").trim(),V=J.file?String(J.file).trim():"";let z=String(J.content??"").trim();if(V){if(z)throw new $("正文位置参数和 --file 只能二选一");const Q=G.resolve(V);if(!R.existsSync(Q))throw new $(`--file 指向的文件不存在: ${Q}`);z=R.readFileSync(Q,"utf-8").trim();if(!z)throw new $(`--file 指向的文件内容为空: ${Q}`)}const Z=String(J.images??"").split(",").map((Q)=>Q.trim()).filter(Boolean),F=J.topics?String(J.topics).split(",").map((Q)=>Q.trim().replace(/^#/,"")).filter(Boolean):[],Y=Boolean(J.draft);if(!K)throw new $("--title 必填");if(K.length>S)throw new $(`标题 ${K.length} 字,需 ≤ ${S} 字`);if(!z)throw new $("缺少正文:给位置参数 <content>,或用 --file <本机文件路径>");T(z,{fileFlag:"--file"});if(z.length>M)throw new $(`正文 ${z.length} 字,需 ≤ ${M} 字`);if(Z.length===0)throw new $("至少提供 1 张图片(--images)");if(Z.length>w)throw new $(`图片过多: ${Z.length}(最多 ${w} 张)`);const O=n(Z),f=F.length?`${z} ${F.map((Q)=>`#${Q}`).join(" ")} `:z;await q.goto(m);let W="";for(let Q=0;Q<30;Q++){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 j(`被重定向离开创作者中心(当前 ${W})——登录态可能已过期。`+"用 ppcli douyin login 重新登录。截图: /tmp/douyin_image_redirect_debug.png")}const k=await i(q,O);if(!k.ok){await q.screenshot({path:"/tmp/douyin_image_upload_debug.png"});throw new j(`图片上传失败: ${k.error??"unknown"}。截图: /tmp/douyin_image_upload_debug.png`)}if(!await p(q)){await q.screenshot({path:"/tmp/douyin_image_form_debug.png"});throw new j("上传后编辑页未就绪(标题输入框未出现),页面结构可能变化。截图: /tmp/douyin_image_form_debug.png")}await q.wait({time:E/1000});await s(q,K);await q.wait({time:0.4});await g(q,f);await q.wait({time:0.6});const I=await r(q,Y);if(!I?.ok){await q.screenshot({path:"/tmp/douyin_image_submit_debug.png"});throw new j(`找不到「${Y?"暂存离开":"发布"}」按钮。可见按钮: ${(I?.seen||[]).join(" | ")||"无"}。截图: /tmp/douyin_image_submit_debug.png`)}const d=Y?["暂存成功","保存成功","存入草稿"]:["发布成功","提交成功"];let B=W,X=!1,v="";for(let Q=0;Q<24;Q++){await q.wait({time:0.5});const H=await q.evaluate(`
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(d)})
64
- `);B=H?.url??B;if(H?.toast){v=H.toast;break}if(B.includes("/content/manage")||Y&&B.includes("/content/draft")){X=!0;break}}let y=null;if(!X&&!v&&B.includes("creator.douyin.com"))try{const H=await c(q,"GET",Y?"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"),u=H.aweme_list??H.data?.work_list??[],U=z.replace(/\s+/g,"").slice(0,20),b=Date.now()/1000;y=u.find((N)=>{const x=String(N.desc??N.title??"").replace(/\s+/g,""),L=Number(N.create_time??0);return L>0&&b-L<900&&(x.includes(K.replace(/\s+/g,""))||U&&x.includes(U))})??null}catch{}if(!X&&!v&&!y){await q.screenshot({path:"/tmp/douyin_image_verify_debug.png"});throw new j(`${Y?"暂存":"发布"}结果无法确认:未跳转作品管理页、未见成功提示,`+`后台${Y?"草稿箱":"作品列表"}回查也没找到本次内容——大概率没发出去,可以安全重试。`+`当前 URL: ${B}。截图: /tmp/douyin_image_verify_debug.png`)}return[{status:`✅ ${Y?"暂存成功":"发布成功"}`,detail:[`"${K}"`,`${O.length}张图片`,F.length?`话题: ${F.map((Q)=>`#${Q}`).join(" ")}`:"",y?.aweme_id?`aweme_id: ${y.aweme_id}(后台回查坐实)`:"",X?B:v].filter(Boolean).join(" · ")}]}});
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(" · ")}]}});
@@ -1,8 +1,27 @@
1
- import{ArgumentError as m,AuthRequiredError as h,CommandExecutionError as s,EmptyResultError as i}from"@jackwener/opencli/errors";import{cli as b,Strategy as k}from"@jackwener/opencli/registry";const x="https://www.facebook.com/",a=50;function c(t){const n=Number(t);if(!Number.isInteger(n)||n<1||n>a)throw new m(`facebook feed --limit must be an integer between 1 and ${a}`);return n}function p(t){if(t&&typeof t==="object"&&"data"in t)return t.data;return t}function l(t){return`(() => {
2
- const limit = ${t};
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
- return String(value || '').replace(/\\s+/g, ' ').trim();
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 combined = dedupe([...primary, ...fallbackContainers()]);
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 u(t,n){const d=c(n.limit??10);try{await t.goto(x,{settleMs:4000})}catch(o){throw new s(`Failed to navigate to facebook feed: ${o instanceof Error?o.message:o}`,"Check that facebook.com is reachable and the browser extension is connected.")}let e;try{e=p(await t.evaluate(l(d)))}catch(o){throw new s(`Failed to read facebook feed: ${o instanceof Error?o.message:o}`,"Facebook may not have rendered or the feed markup may have changed.")}if(!e||typeof e!=="object"||!Array.isArray(e.rows))throw new s("facebook feed returned malformed extraction payload");if(e.status==="auth")throw new h("www.facebook.com","Open Chrome and log in to Facebook before retrying.");if(e.rows.length>0)return e.rows;if(e.status==="empty")throw new i("facebook feed","Facebook did not show any feed posts for this account.");const r=e.diagnostics||{};if(r.articleCount||r.fallbackActionCount||r.mainTextLength>200)throw new s("facebook feed page rendered but no feed rows could be extracted",`Diagnostics: articles=${r.articleCount||0}, actions=${r.fallbackActionCount||0}, mainTextLength=${r.mainTextLength||0}.`);throw new i("facebook feed","No Facebook feed content was visible in the current browser session.")}const f={site:"facebook",name:"feed",access:"read",description:"Get your Facebook news feed",domain:"www.facebook.com",strategy:k.COOKIE,browser:!0,navigateBefore:!1,args:[{name:"limit",type:"int",default:10,help:"Number of posts"}],columns:["index","author","content","likes","comments","shares"],func:u};b(f);export const __test__={buildFeedExtractScript:l,command:f,getFacebookFeed:u,requireLimit:c};
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};
@@ -1,11 +1,11 @@
1
- import*as v from"node:fs";import*as H from"node:path";import{ArgumentError as V,AuthRequiredError as A,CommandExecutionError as $}from"@jackwener/opencli/errors";import{cli as N,Strategy as L}from"@jackwener/opencli/registry";import{assertLiteralContent as M}from"../_shared/content-guard.js";const y='[data-lexical-editor="true"][contenteditable="true"]',B='[data-pp-fb-composer="1"]',_='[data-pp-fb-trigger="1"]',U='[data-pp-fb-post="1"]',k="https://www.facebook.com/",I=new Set([".jpg",".jpeg",".png",".gif",".webp"]);function T(z){const J=String(z??"").trim();if(!J)return k;if(/^https?:\/\//i.test(J)){let K;try{K=new URL(J)}catch{throw new V(`Invalid target URL: ${J}`)}if(K.hostname!=="facebook.com"&&!K.hostname.endsWith(".facebook.com"))throw new V("target URL must be on facebook.com.");return K.href}const Q=J.replace(/^@/,"").replace(/^\/+|\/+$/g,"");if(!/^[A-Za-z0-9.\-]+$/.test(Q))throw new V(`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 K=H.resolve(Q),Y=H.extname(K).toLowerCase();if(!I.has(Y))throw new $(`Unsupported image format "${Y}". Supported: jpg, png, gif, webp`);const Z=v.statSync(K,{throwIfNoEntry:!1});if(!Z||!Z.isFile())throw new $(`Not a valid file: ${K}`);return K})}function R(){return`(() => {
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('${y}') || document.querySelector('${y}');
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('${B}');
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 O(z){return`(() => {
49
+ })()`}function b(z){return`(() => {
50
50
  const collapse = (s) => String(s || '').replace(/\\s+/g, ' ').trim();
51
- const ed = document.querySelector('${B}');
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 D='[role="dialog"] input[type="file"], input[type="file"][accept*="image"]';async function C(z,J){const Q=J.map((Y)=>{const Z=H.extname(Y).toLowerCase(),q=Z===".png"?"image/png":Z===".gif"?"image/gif":Z===".webp"?"image/webp":"image/jpeg";return{name:H.basename(Y),mime:q,base64:v.readFileSync(Y).toString("base64")}}),K=await z.evaluate(`(() => {
58
- const input = document.querySelector(${JSON.stringify(D)});
59
- if (!input) return { ok: false, error: 'No file input found in composer' };
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(X===null||!G(X).includes(G(Q)))throw new $(`Composed text did not land before publish (got ${JSON.stringify((X||"").slice(0,60))}).`);const j=await z.evaluate(S());if(j?.notfound)throw new $("Post button not found in the composer.");if(j?.disabled)throw new $("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(O(Q)))?.ok)throw new $("Post still in the composer after clicking Post — publish blocked (rate limit, restricted, or no permission). Not confirmed sent; verify before retrying.");const f=K.length>0?` with ${K.length} image(s)`:"";return[{status:"success",target:Y,message:`Post published${f}.`}]}});
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}.`}]}});