publishport-opencli 1.8.5-pp.13 → 1.8.5-pp.14

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 CHANGED
@@ -45507,9 +45507,15 @@
45507
45507
  {
45508
45508
  "name": "content",
45509
45509
  "type": "str",
45510
- "required": true,
45510
+ "required": false,
45511
45511
  "positional": true,
45512
- "help": "笔记正文"
45512
+ "help": "笔记正文(字面文本,不会展开 @文件 引用;长正文建议用 --file)"
45513
+ },
45514
+ {
45515
+ "name": "file",
45516
+ "type": "str",
45517
+ "required": false,
45518
+ "help": "从本机文件读取笔记正文(UTF-8),与位置参数 <content> 二选一"
45513
45519
  },
45514
45520
  {
45515
45521
  "name": "title",
@@ -1,10 +1,10 @@
1
- import{CommandExecutionError as O}from"@jackwener/opencli/errors";import{normalizeContent as H}from"./format.js";import{inlineLocalImages as L,isLocalImagePath as C,localImageToDataUri as M}from"./images.js";import{PAGE_RUNTIME as I}from"./page-runtime.js";export function selectContent(B,K,V="auto"){const Z=H(B,{format:V});return K.outputFormat==="markdown"?Z.markdown:Z.html}export function originReFromHome(B){let K="";try{K=new URL(B).host}catch(Z){K=""}return"^https?://([^/]*\\.)?"+K.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")+"(/|$)"}export async function gotoWritePage(B,K,V){const Z=V||originReFromHome(K);for(let G=0;G<8;G++){await B.goto(K);await B.wait({time:1});const D=await B.evaluate(`(() => {
1
+ import{CommandExecutionError as O}from"@jackwener/opencli/errors";import{normalizeContent as H}from"./format.js";import{assertLiteralContent as M}from"../content-guard.js";import{inlineLocalImages as U,isLocalImagePath as C,localImageToDataUri as I}from"./images.js";import{PAGE_RUNTIME as E}from"./page-runtime.js";export function selectContent(B,K,V="auto"){const Z=H(B,{format:V});return K.outputFormat==="markdown"?Z.markdown:Z.html}export function originReFromHome(B){let K="";try{K=new URL(B).host}catch(Z){K=""}return"^https?://([^/]*\\.)?"+K.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")+"(/|$)"}export async function gotoWritePage(B,K,V){const Z=V||originReFromHome(K);for(let G=0;G<8;G++){await B.goto(K);await B.wait({time:1});const D=await B.evaluate(`(() => {
2
2
  var href = ''; try { href = location.href; } catch (e) {}
3
3
  var onSite = new RegExp(${JSON.stringify(Z)}).test(href);
4
4
  var cookieOk = true; try { void document.cookie; } catch (e) { cookieOk = false; }
5
5
  return { href: href, onSite: onSite, cookieOk: cookieOk };
6
6
  })()`);if(D&&D.onSite&&D.cookieOk)return;await B.wait({time:1})}throw new O(`无法在浏览器中打开已登录的写作页(页面停留在空白页,未落到 ${K})。`+"请确认登录该平台的 Chrome 已连接 opencli 浏览器桥。")}export function buildPublishJs(B,K,V){return`(async () => {
7
- `+I+`
7
+ `+E+`
8
8
  const I = `+JSON.stringify(B)+`;
9
9
  const __publish = (`+K+`);
10
10
  `+(V?"const __upload = ("+V+`);
@@ -31,4 +31,4 @@ if (!__pub || __pub.ok === false) {
31
31
  return { ok: false, stage: (__pub && __pub.stage) || "publish", status: __pub && __pub.status, message: (__pub && __pub.message) || "publish failed", uploaded: __upN, failed: __failN };
32
32
  }
33
33
  return { ok: true, id: String(__pub.id == null ? "" : __pub.id), url: __pub.url || "", draft: !!__pub.draft, uploaded: __upN, failed: __failN };
34
- })()`}export async function publishArticle(B,K){const{title:V,body:Z,format:G="auto",draftOnly:D=!1,profile:q,publishParams:U=null}=K;if(!q)throw Error("publishArticle: profile is required");if(!q.home)throw Error("publishArticle: profile.home is required");if(typeof q.publish!=="function")throw Error("publishArticle: profile.publish must be a function");const W=H(Z,{format:G}),k=q.outputFormat==="markdown"||q.needsMarkdown===!0,w=q.outputFormat==="html"||q.needsHtml===!0,_=k?await L(W.markdown):{content:null,missing:[]},z=w?await L(W.html):{content:null,missing:[]},A=_.content,J=z.content,y=q.outputFormat==="markdown"?A:J,N=[],T=new Set;for(const Q of[..._.missing,...z.missing]){if(T.has(Q.src))continue;T.add(Q.src);N.push(Q)}let Y=U;if(Y&&typeof Y.cover==="string"&&Y.cover){const Q=Y.cover.trim();if(/["<>\s]/.test(Q))throw new O("封面图路径/URL 含非法字符(引号/尖括号/空白):"+Q.slice(0,120));if(C(Q))try{const{dataUri:$}=await M(Q);Y={...Y,cover:$}}catch($){throw new O("封面图读取失败:"+Q+"("+String($&&$.message||$)+")")}else Y={...Y,cover:Q}}await gotoWritePage(B,q.home,q.originRe);const j={title:V,content:y,markdown:q.needsMarkdown===!0&&q.outputFormat!=="markdown"?A:null,html:q.needsHtml===!0&&q.outputFormat!=="html"?J:null,draftOnly:!!D,outputFormat:q.outputFormat,preprocessConfig:q.preprocessConfig||null,imageSpec:q.image?.spec||null,imageSkip:q.image?.skip||[],publishParams:Y},R=typeof q.image?.uploadFn==="function"?q.image.uploadFn.toString():null,x=buildPublishJs(j,q.publish.toString(),R),X=await B.evaluate(x);if(!X||X.ok===!1){const Q=X?.stage||"publish",$=X?.status!=null?` (HTTP ${X.status})`:"";throw new O(`[${Q}] ${X?.message||"发布失败"}${$}`)}return{id:X.id,url:X.url,draft:X.draft,images:{uploaded:X.uploaded||[],failed:(X.failed||[]).concat(N)}}}export const __test__={selectContent,originReFromHome,buildPublishJs,publishArticle};
34
+ })()`}export async function publishArticle(B,K){const{title:V,body:Z,format:G="auto",draftOnly:D=!1,profile:q,publishParams:k=null}=K;if(!q)throw Error("publishArticle: profile is required");if(!q.home)throw Error("publishArticle: profile.home is required");if(typeof q.publish!=="function")throw Error("publishArticle: profile.publish must be a function");M(Z,{fileFlag:"--file"});const W=H(Z,{format:G}),w=q.outputFormat==="markdown"||q.needsMarkdown===!0,L=q.outputFormat==="html"||q.needsHtml===!0,_=w?await U(W.markdown):{content:null,missing:[]},z=L?await U(W.html):{content:null,missing:[]},A=_.content,J=z.content,y=q.outputFormat==="markdown"?A:J,N=[],T=new Set;for(const Q of[..._.missing,...z.missing]){if(T.has(Q.src))continue;T.add(Q.src);N.push(Q)}let Y=k;if(Y&&typeof Y.cover==="string"&&Y.cover){const Q=Y.cover.trim();if(/["<>\s]/.test(Q))throw new O("封面图路径/URL 含非法字符(引号/尖括号/空白):"+Q.slice(0,120));if(C(Q))try{const{dataUri:$}=await I(Q);Y={...Y,cover:$}}catch($){throw new O("封面图读取失败:"+Q+"("+String($&&$.message||$)+")")}else Y={...Y,cover:Q}}await gotoWritePage(B,q.home,q.originRe);const R={title:V,content:y,markdown:q.needsMarkdown===!0&&q.outputFormat!=="markdown"?A:null,html:q.needsHtml===!0&&q.outputFormat!=="html"?J:null,draftOnly:!!D,outputFormat:q.outputFormat,preprocessConfig:q.preprocessConfig||null,imageSpec:q.image?.spec||null,imageSkip:q.image?.skip||[],publishParams:Y},j=typeof q.image?.uploadFn==="function"?q.image.uploadFn.toString():null,x=buildPublishJs(R,q.publish.toString(),j),X=await B.evaluate(x);if(!X||X.ok===!1){const Q=X?.stage||"publish",$=X?.status!=null?` (HTTP ${X.status})`:"";throw new O(`[${Q}] ${X?.message||"发布失败"}${$}`)}return{id:X.id,url:X.url,draft:X.draft,images:{uploaded:X.uploaded||[],failed:(X.failed||[]).concat(N)}}}export const __test__={selectContent,originReFromHome,buildPublishJs,publishArticle};
@@ -0,0 +1 @@
1
+ import*as J from"node:fs";import*as K from"node:os";import{ArgumentError as G}from"@jackwener/opencli/errors";const M=/^@(?:\/|~\/|\.{1,2}\/)\S+$/,N=/^(?:\/|~\/)\S+$/;function O(y){return y.startsWith("~/")?`${K.homedir()}/${y.slice(2)}`:y}export function assertLiteralContent(y,{fileFlag:C=null,label:q="正文"}={}){const h=String(y??"").trim();if(!h)return h;const D=C?(z)=>`请改用 ${C} ${z},或把文件内容读出来作为${q}传入`:()=>`本命令没有文件参数,请把文件内容读出来作为${q}传入`;if(M.test(h))throw new G(`${q} "${h}" 看起来是文件引用——命令不会展开 @路径,照发会把这串字面量当${q}。${D(h.slice(1))}`);if(N.test(h)){let z=!1;try{z=J.statSync(O(h)).isFile()}catch{}if(z)throw new G(`${q} "${h}" 是一个本机已存在的文件路径——照发会把这串路径当${q}发出去。${D(h)}`)}return h}
@@ -1 +1 @@
1
- import{cli as B,Strategy as G}from"@jackwener/opencli/registry";import{ArgumentError as Q,CommandExecutionError as V,EmptyResultError as I}from"@jackwener/opencli/errors";import{apiGet as S,apiPost as U,requireOkPayload as Z,resolveBvid as q,resolveUid as O}from"./utils.js";function R(D,f){const z=Number(D);if(!Number.isInteger(z)||z<=0)throw new Q(`bilibili comment ${f} must be a positive integer`);return z}B({site:"bilibili",name:"comment",access:"write",description:"在 B站视频下发表评论或回复(官方 API,需登录;消息里的 @用户 会被解析为真实提及)",domain:"www.bilibili.com",strategy:G.COOKIE,args:[{name:"bvid",required:!0,positional:!0,help:"Video BV ID / URL / b23.tv short link"},{name:"message",required:!0,positional:!0,help:"Comment text. Any @username in it is resolved to a real mention"},{name:"parent",type:"int",help:"top-level/root rpid to reply under (omit for a top-level comment)"},{name:"execute",type:"boolean",help:"Actually post the comment. Without it the command refuses to write."}],columns:["rpid","bvid","oid","message","url"],func:async(D,f)=>{if(!D)throw new V("Browser session required for bilibili comment");const z=String(f.message??"").trim();if(!z)throw new Q("bilibili comment message cannot be empty");if(!f.execute)throw new Q("Refusing to post: pass --execute to actually publish this comment");const W=f.parent!=null?R(f.parent,"parent"):null;let H;try{H=await q(f.bvid)}catch(J){throw new Q(`Cannot resolve Bilibili BV ID from input: ${String(f.bvid??"")}`,J instanceof Error?J.message:String(J))}const _=await S(D,"/x/web-interface/view",{params:{bvid:H}}),X=Z(_,"view")?.aid;if(!X)throw new V(`Cannot resolve aid for bvid: ${H}`);const K={};for(const J of z.matchAll(/@([^\s@]+)/g)){const L=J[1];if(L in K)continue;try{const F=Number(await O(D,L));if(!Number.isInteger(F)||F<=0)throw new V(`Bilibili user search returned malformed mid for @${L}`);K[L]=F}catch(F){if(!(F instanceof I))throw F}}const $={oid:X,type:1,message:z,plat:1,...W!=null?{root:W,parent:W}:{},...Object.keys(K).length>0?{at_name_to_mid:JSON.stringify(K)}:{}},h=await U(D,"/x/v2/reply/add",{params:$}),Y=Z(h,"reply add")?.rpid;if(!Y)throw new V("Bilibili reply add API did not return rpid for the posted comment");return[{rpid:String(Y),bvid:H,oid:String(X),message:z,url:`https://www.bilibili.com/video/${H}#reply${Y}`}]}});
1
+ import{cli as L,Strategy as S}from"@jackwener/opencli/registry";import{ArgumentError as V,CommandExecutionError as W,EmptyResultError as U}from"@jackwener/opencli/errors";import{apiGet as _,apiPost as h,requireOkPayload as $,resolveBvid as q,resolveUid as O}from"./utils.js";import{assertLiteralContent as R}from"../_shared/content-guard.js";function j(D,z){const f=Number(D);if(!Number.isInteger(f)||f<=0)throw new V(`bilibili comment ${z} must be a positive integer`);return f}L({site:"bilibili",name:"comment",access:"write",description:"在 B站视频下发表评论或回复(官方 API,需登录;消息里的 @用户 会被解析为真实提及)",domain:"www.bilibili.com",strategy:S.COOKIE,args:[{name:"bvid",required:!0,positional:!0,help:"Video BV ID / URL / b23.tv short link"},{name:"message",required:!0,positional:!0,help:"Comment text. Any @username in it is resolved to a real mention"},{name:"parent",type:"int",help:"top-level/root rpid to reply under (omit for a top-level comment)"},{name:"execute",type:"boolean",help:"Actually post the comment. Without it the command refuses to write."}],columns:["rpid","bvid","oid","message","url"],func:async(D,z)=>{if(!D)throw new W("Browser session required for bilibili comment");const f=String(z.message??"").trim();R(f,{label:"评论内容"});if(!f)throw new V("bilibili comment message cannot be empty");if(!z.execute)throw new V("Refusing to post: pass --execute to actually publish this comment");const X=z.parent!=null?j(z.parent,"parent"):null;let H;try{H=await q(z.bvid)}catch(J){throw new V(`Cannot resolve Bilibili BV ID from input: ${String(z.bvid??"")}`,J instanceof Error?J.message:String(J))}const B=await _(D,"/x/web-interface/view",{params:{bvid:H}}),Y=$(B,"view")?.aid;if(!Y)throw new W(`Cannot resolve aid for bvid: ${H}`);const K={};for(const J of f.matchAll(/@([^\s@]+)/g)){const Q=J[1];if(Q in K)continue;try{const F=Number(await O(D,Q));if(!Number.isInteger(F)||F<=0)throw new W(`Bilibili user search returned malformed mid for @${Q}`);K[Q]=F}catch(F){if(!(F instanceof U))throw F}}const G={oid:Y,type:1,message:f,plat:1,...X!=null?{root:X,parent:X}:{},...Object.keys(K).length>0?{at_name_to_mid:JSON.stringify(K)}:{}},I=await h(D,"/x/v2/reply/add",{params:G}),Z=$(I,"reply add")?.rpid;if(!Z)throw new W("Bilibili reply add API did not return rpid for the posted comment");return[{rpid:String(Z),bvid:H,oid:String(Y),message:f,url:`https://www.bilibili.com/video/${H}#reply${Z}`}]}});
@@ -1,4 +1,4 @@
1
- import{cli as K,Strategy as M}from"@jackwener/opencli/registry";import{ArgumentError as J,CommandExecutionError as B}from"@jackwener/opencli/errors";K({site:"bilibili",name:"dynamic-post",access:"write",description:"发布 B站动态(纯文本,官方 API,需登录)",domain:"www.bilibili.com",strategy:M.COOKIE,browser:!0,args:[{name:"text",required:!0,positional:!0,help:"Dynamic text content"},{name:"execute",type:"boolean",help:"Actually publish. Without it the command refuses to write."}],columns:["status","dynamic_id","text","url"],func:async(q,D)=>{if(!q)throw new B("Browser session required for bilibili dynamic-post");const z=String(D.text??"").trim();if(!z)throw new J("bilibili dynamic-post text cannot be empty");if(!D.execute)throw new J("Refusing to post: pass --execute to actually publish this dynamic");let G=!1;for(let H=0;H<4;H++){await q.goto("https://t.bilibili.com");await q.wait(2);const I=await q.evaluate("location.href");if(typeof I==="string"&&/^https?:\/\/[^/]*bilibili\.com/.test(I)){G=!0;break}}if(!G)throw new B("Failed to navigate to t.bilibili.com (page did not land on a bilibili origin)");const v=await q.evaluate(`(async () => {
1
+ import{cli as K,Strategy as M}from"@jackwener/opencli/registry";import{ArgumentError as J,CommandExecutionError as B}from"@jackwener/opencli/errors";import{assertLiteralContent as N}from"../_shared/content-guard.js";K({site:"bilibili",name:"dynamic-post",access:"write",description:"发布 B站动态(纯文本,官方 API,需登录)",domain:"www.bilibili.com",strategy:M.COOKIE,browser:!0,args:[{name:"text",required:!0,positional:!0,help:"Dynamic text content"},{name:"execute",type:"boolean",help:"Actually publish. Without it the command refuses to write."}],columns:["status","dynamic_id","text","url"],func:async(q,D)=>{if(!q)throw new B("Browser session required for bilibili dynamic-post");const z=String(D.text??"").trim();N(z,{label:"动态内容"});if(!z)throw new J("bilibili dynamic-post text cannot be empty");if(!D.execute)throw new J("Refusing to post: pass --execute to actually publish this dynamic");let G=!1;for(let H=0;H<4;H++){await q.goto("https://t.bilibili.com");await q.wait(2);const I=await q.evaluate("location.href");if(typeof I==="string"&&/^https?:\/\/[^/]*bilibili\.com/.test(I)){G=!0;break}}if(!G)throw new B("Failed to navigate to t.bilibili.com (page did not land on a bilibili origin)");const v=await q.evaluate(`(async () => {
2
2
  var text = ${JSON.stringify(z)};
3
3
  var csrf = (document.cookie.match(/bili_jct=([^;]+)/) || [])[1] || '';
4
4
  var url = 'https://api.bilibili.com/x/dynamic/feed/create/dyn?csrf=' + encodeURIComponent(csrf);
@@ -1,4 +1,4 @@
1
- import*as V from"node:fs";import{cli as q,Strategy as B}from"@jackwener/opencli/registry";import{ArgumentError as K,AuthRequiredError as U,CommandExecutionError as O}from"@jackwener/opencli/errors";const W=4;export function resolveBody(z){if(z["body-file"]){const D=String(z["body-file"]);if(!V.statSync(D,{throwIfNoEntry:!1})?.isFile())throw new K(`--body-file not found: ${D}`);return V.readFileSync(D,"utf8")}if(z.body!==void 0&&z.body!==null)return String(z.body);return}export function resolveTagList(z){if(z===void 0||z===null||z==="")return;const D=String(z).split(",").map((F)=>F.trim().toLowerCase()).filter(Boolean);if(D.length>W)throw new K(`Too many tags: ${D.length} (DEV.to max ${W})`);for(const F of D)if(!/^[a-z0-9]+$/.test(F))throw new K(`Invalid tag "${F}". DEV.to tags must be lowercase alphanumeric (no spaces/punctuation).`);return D.join(", ")}export function buildArticle(z,D,F){const H={};if(z.title!==void 0)H.title=String(z.title);if(D!==void 0)H.body_markdown=D;if(z.published!==void 0)H.published=!!z.published;if(F!==void 0)H.tag_list=F;if(z["cover-image"])H.main_image=String(z["cover-image"]);if(z["canonical-url"])H.canonical_url=String(z["canonical-url"]);if(z.series)H.series=String(z.series);if(z.description)H.description=String(z.description);return H}q({site:"devto",name:"publish",access:"write",description:"Publish or update a DEV.to article (markdown). Draft by default; pass --published to go live.",domain:"dev.to",strategy:B.COOKIE,browser:!0,args:[{name:"title",type:"string",required:!1,help:"Article title (required for new articles)"},{name:"body",type:"string",required:!1,help:"Article body in Markdown (or use --body-file)"},{name:"body-file",type:"string",required:!1,help:"Path to a Markdown file for the body"},{name:"published",type:"boolean",required:!1,default:!1,help:"Publish live now (default: save as draft)"},{name:"tags",type:"string",required:!1,help:"Comma-separated tags, max 4, lowercase alphanumeric"},{name:"cover-image",type:"string",required:!1,help:"Cover image URL (main_image)"},{name:"canonical-url",type:"string",required:!1,help:"Canonical URL (original source)"},{name:"series",type:"string",required:!1,help:"Series name to group articles"},{name:"description",type:"string",required:!1,help:"Social/SEO description"},{name:"id",type:"string",required:!1,help:"Existing article id to update (PUT instead of POST)"}],columns:["status","id","url","published","slug"],func:async(z,D)=>{if(!z)throw new O("Browser session required for devto publish");const F=D.id?String(D.id).trim():"",H=resolveBody(D),Y=resolveTagList(D.tags);if(!F&&!D.title)throw new K("--title is required when creating a new article");if(!F&&H===void 0)throw new K("--body or --body-file is required when creating a new article");const P=buildArticle(D,H,Y),Z=F?"PUT":"POST",$=F?`/articles/${encodeURIComponent(F)}`:"/articles";await z.goto("https://dev.to/");const I=await z.evaluate(`(async () => {
1
+ import*as V from"node:fs";import{cli as q,Strategy as B}from"@jackwener/opencli/registry";import{ArgumentError as K,AuthRequiredError as U,CommandExecutionError as O}from"@jackwener/opencli/errors";import{assertLiteralContent as v}from"../_shared/content-guard.js";const W=4;export function resolveBody(z){if(z["body-file"]){const D=String(z["body-file"]);if(!V.statSync(D,{throwIfNoEntry:!1})?.isFile())throw new K(`--body-file not found: ${D}`);return V.readFileSync(D,"utf8")}if(z.body!==void 0&&z.body!==null){v(String(z.body),{fileFlag:"--body-file"});return String(z.body)}return}export function resolveTagList(z){if(z===void 0||z===null||z==="")return;const D=String(z).split(",").map((F)=>F.trim().toLowerCase()).filter(Boolean);if(D.length>W)throw new K(`Too many tags: ${D.length} (DEV.to max ${W})`);for(const F of D)if(!/^[a-z0-9]+$/.test(F))throw new K(`Invalid tag "${F}". DEV.to tags must be lowercase alphanumeric (no spaces/punctuation).`);return D.join(", ")}export function buildArticle(z,D,F){const H={};if(z.title!==void 0)H.title=String(z.title);if(D!==void 0)H.body_markdown=D;if(z.published!==void 0)H.published=!!z.published;if(F!==void 0)H.tag_list=F;if(z["cover-image"])H.main_image=String(z["cover-image"]);if(z["canonical-url"])H.canonical_url=String(z["canonical-url"]);if(z.series)H.series=String(z.series);if(z.description)H.description=String(z.description);return H}q({site:"devto",name:"publish",access:"write",description:"Publish or update a DEV.to article (markdown). Draft by default; pass --published to go live.",domain:"dev.to",strategy:B.COOKIE,browser:!0,args:[{name:"title",type:"string",required:!1,help:"Article title (required for new articles)"},{name:"body",type:"string",required:!1,help:"Article body in Markdown (or use --body-file)"},{name:"body-file",type:"string",required:!1,help:"Path to a Markdown file for the body"},{name:"published",type:"boolean",required:!1,default:!1,help:"Publish live now (default: save as draft)"},{name:"tags",type:"string",required:!1,help:"Comma-separated tags, max 4, lowercase alphanumeric"},{name:"cover-image",type:"string",required:!1,help:"Cover image URL (main_image)"},{name:"canonical-url",type:"string",required:!1,help:"Canonical URL (original source)"},{name:"series",type:"string",required:!1,help:"Series name to group articles"},{name:"description",type:"string",required:!1,help:"Social/SEO description"},{name:"id",type:"string",required:!1,help:"Existing article id to update (PUT instead of POST)"}],columns:["status","id","url","published","slug"],func:async(z,D)=>{if(!z)throw new O("Browser session required for devto publish");const F=D.id?String(D.id).trim():"",H=resolveBody(D),Y=resolveTagList(D.tags);if(!F&&!D.title)throw new K("--title is required when creating a new article");if(!F&&H===void 0)throw new K("--body or --body-file is required when creating a new article");const P=buildArticle(D,H,Y),Z=F?"PUT":"POST",$=F?`/articles/${encodeURIComponent(F)}`:"/articles";await z.goto("https://dev.to/");const I=await z.evaluate(`(async () => {
2
2
  try {
3
3
  var csrf = (document.querySelector('meta[name="csrf-token"]') || {}).content || window.csrfToken;
4
4
  if (!csrf) return { kind: 'auth', detail: 'no csrf-token meta — not logged in?' };
@@ -14,4 +14,4 @@ import*as V from"node:fs";import{cli as q,Strategy as B}from"@jackwener/opencli/
14
14
  if (!r.ok) return { kind: 'http', status: r.status, detail: text.slice(0, 300) };
15
15
  return { kind: 'ok', data: data };
16
16
  } catch (e) { return { kind: 'exception', detail: String(e && e.message || e) }; }
17
- })()`);if(I?.kind==="auth")throw new U("dev.to",`Not logged in (${I.detail}). Run \`opencli devto login\`.`);if(I?.kind==="http")throw new O(`dev.to publish failed: HTTP ${I.status} ${I.detail}`);if(I?.kind==="exception")throw new O(`dev.to publish error: ${I.detail}`);if(I?.kind!=="ok"||!I.data)throw new O(`Unexpected dev.to response: ${JSON.stringify(I)}`);const J=I.data,Q=!!P.published;let N="";if(J.current_state_path)try{N=new URL(J.current_state_path,"https://dev.to").href}catch(j){N=String(J.current_state_path)}else if(J.url)N=String(J.url);return[{status:F?"updated":"created",id:J.id??"",url:Q?N:"",published:Q,slug:J.slug??""}]}});
17
+ })()`);if(I?.kind==="auth")throw new U("dev.to",`Not logged in (${I.detail}). Run \`opencli devto login\`.`);if(I?.kind==="http")throw new O(`dev.to publish failed: HTTP ${I.status} ${I.detail}`);if(I?.kind==="exception")throw new O(`dev.to publish error: ${I.detail}`);if(I?.kind!=="ok"||!I.data)throw new O(`Unexpected dev.to response: ${JSON.stringify(I)}`);const J=I.data,Q=!!P.published;let N="";if(J.current_state_path)try{N=new URL(J.current_state_path,"https://dev.to").href}catch(G){N=String(J.current_state_path)}else if(J.url)N=String(J.url);return[{status:F?"updated":"created",id:J.id??"",url:Q?N:"",published:Q,slug:J.slug??""}]}});
@@ -1,4 +1,4 @@
1
- import*as M from"node:fs";import{cli as I,Strategy as S}from"@jackwener/opencli/registry";import{ArgumentError as j,AuthRequiredError as b,CommandExecutionError as V}from"@jackwener/opencli/errors";const B="https://hashnode.com";export function resolveBody(z){if(z["body-file"]){const D=String(z["body-file"]);if(!M.statSync(D,{throwIfNoEntry:!1})?.isFile())throw new j(`--body-file not found: ${D}`);return M.readFileSync(D,"utf8")}if(z.body!==void 0&&z.body!==null)return String(z.body);return}export function resolveTags(z){if(z===void 0||z===null||z==="")return[];return[...new Set(String(z).split(",").map((D)=>D.trim()).filter(Boolean))]}const h=`(async () => {
1
+ import*as M from"node:fs";import{cli as I,Strategy as S}from"@jackwener/opencli/registry";import{ArgumentError as _,AuthRequiredError as b,CommandExecutionError as V}from"@jackwener/opencli/errors";import{assertLiteralContent as O}from"../_shared/content-guard.js";const j="https://hashnode.com";export function resolveBody(z){if(z["body-file"]){const D=String(z["body-file"]);if(!M.statSync(D,{throwIfNoEntry:!1})?.isFile())throw new _(`--body-file not found: ${D}`);return M.readFileSync(D,"utf8")}if(z.body!==void 0&&z.body!==null){O(String(z.body),{fileFlag:"--body-file"});return String(z.body)}return}export function resolveTags(z){if(z===void 0||z===null||z==="")return[];return[...new Set(String(z).split(",").map((D)=>D.trim()).filter(Boolean))]}const h=`(async () => {
2
2
  if (location.href.indexOf('hashnode.com') < 0) return { drift: true };
3
3
  try {
4
4
  var r = await fetch('/api/auth/session', { credentials: 'include', headers: { Accept: 'application/json' } });
@@ -6,7 +6,7 @@ import*as M from"node:fs";import{cli as I,Strategy as S}from"@jackwener/opencli/
6
6
  var u = j && j.user;
7
7
  return u && u.username ? { ok: true, username: String(u.username) } : { ok: false };
8
8
  } catch (e) { return { drift: true, err: String(e && e.message || e) }; }
9
- })()`;async function W(z,D,{tries:Y=8,label:Z="eval",anchor:$=B}={}){let q;for(let T=0;T<Y;T+=1){let F;try{F=await z.evaluate(D)}catch(G){F={drift:!0,err:String(G&&G.message||G)}}q=F;if(F&&F.drift){await z.goto($);await z.wait(2);continue}return F}throw new V(`Hashnode ${Z}: browser tab kept drifting (last: ${JSON.stringify(q)})`)}I({site:"hashnode",name:"publish",access:"write",description:"Publish a Hashnode post via the web editor (or save a draft with --draft). Markdown body + tags/cover/canonical.",domain:"hashnode.com",strategy:S.COOKIE,browser:!0,defaultWindowMode:"foreground",siteSession:"persistent",args:[{name:"title",type:"string",required:!0,help:"Post title"},{name:"body",type:"string",required:!1,help:"Post body in Markdown (or use --body-file)"},{name:"body-file",type:"string",required:!1,help:"Path to a Markdown file for the body"},{name:"tags",type:"string",required:!1,help:"Comma-separated tag names"},{name:"cover-image",type:"string",required:!1,help:"Cover image URL"},{name:"canonical-url",type:"string",required:!1,help:"Canonical / original article URL"},{name:"draft",type:"boolean",required:!1,default:!1,help:"Save as draft instead of publishing"}],columns:["status","draft_id","url"],func:async(z,D)=>{if(!z)throw new V("Browser session required for hashnode publish");const Y=String(D.title??"").trim();if(!Y)throw new j("hashnode publish: --title is required");const Z=resolveBody(D);if(Z===void 0)throw new j("--body or --body-file is required");const $=resolveTags(D.tags),q=D["canonical-url"]?String(D["canonical-url"]):"",T=D["cover-image"]?String(D["cover-image"]):"";await z.goto(B);const F=await W(z,h,{label:"auth"});if(!F||!F.ok)throw new b("hashnode.com","Not logged in. Run `opencli hashnode login`.");const G=await W(z,`(async () => {
9
+ })()`;async function W(z,D,{tries:Y=8,label:Z="eval",anchor:$=j}={}){let q;for(let B=0;B<Y;B+=1){let F;try{F=await z.evaluate(D)}catch(G){F={drift:!0,err:String(G&&G.message||G)}}q=F;if(F&&F.drift){await z.goto($);await z.wait(2);continue}return F}throw new V(`Hashnode ${Z}: browser tab kept drifting (last: ${JSON.stringify(q)})`)}I({site:"hashnode",name:"publish",access:"write",description:"Publish a Hashnode post via the web editor (or save a draft with --draft). Markdown body + tags/cover/canonical.",domain:"hashnode.com",strategy:S.COOKIE,browser:!0,defaultWindowMode:"foreground",siteSession:"persistent",args:[{name:"title",type:"string",required:!0,help:"Post title"},{name:"body",type:"string",required:!1,help:"Post body in Markdown (or use --body-file)"},{name:"body-file",type:"string",required:!1,help:"Path to a Markdown file for the body"},{name:"tags",type:"string",required:!1,help:"Comma-separated tag names"},{name:"cover-image",type:"string",required:!1,help:"Cover image URL"},{name:"canonical-url",type:"string",required:!1,help:"Canonical / original article URL"},{name:"draft",type:"boolean",required:!1,default:!1,help:"Save as draft instead of publishing"}],columns:["status","draft_id","url"],func:async(z,D)=>{if(!z)throw new V("Browser session required for hashnode publish");const Y=String(D.title??"").trim();if(!Y)throw new _("hashnode publish: --title is required");const Z=resolveBody(D);if(Z===void 0)throw new _("--body or --body-file is required");const $=resolveTags(D.tags),q=D["canonical-url"]?String(D["canonical-url"]):"",B=D["cover-image"]?String(D["cover-image"]):"";await z.goto(j);const F=await W(z,h,{label:"auth"});if(!F||!F.ok)throw new b("hashnode.com","Not logged in. Run `opencli hashnode login`.");const G=await W(z,`(async () => {
10
10
  if (location.href.indexOf('hashnode.com') < 0) return { drift: true };
11
11
  const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
12
12
  const w = [...document.querySelectorAll('a,button')].find((e) => (e.innerText || '').trim() === 'Write');
@@ -16,7 +16,7 @@ import*as M from"node:fs";import{cli as I,Strategy as S}from"@jackwener/opencli/
16
16
  while (t < 18000) { await sleep(500); t += 500; if (location.href.indexOf('/draft/') >= 0) break; }
17
17
  const m = location.href.match(/\\/draft\\/([a-z0-9]+)/i);
18
18
  return m ? { ok: true, draftId: m[1], href: location.href } : { ok: false, reason: 'editor did not open (no /draft/ url) — does the account have a publication?' };
19
- })()`,{label:"new-draft"});if(!G||!G.ok)throw new V(`Hashnode could not open the editor: ${G?G.reason:"unknown"}`);const _=G.draftId,Q=`${B}/draft/${_}`;await z.goto(Q);const J=await W(z,`(async () => {
19
+ })()`,{label:"new-draft"});if(!G||!G.ok)throw new V(`Hashnode could not open the editor: ${G?G.reason:"unknown"}`);const L=G.draftId,Q=`${j}/draft/${L}`;await z.goto(Q);const J=await W(z,`(async () => {
20
20
  if (location.href.indexOf('/draft/') < 0) return { drift: true };
21
21
  const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
22
22
  let t = 0, title, body;
@@ -36,7 +36,7 @@ import*as M from"node:fs";import{cli as I,Strategy as S}from"@jackwener/opencli/
36
36
  document.execCommand('insertText', false, ${JSON.stringify(Z)});
37
37
  await sleep(1500);
38
38
  return { ok: true, titleVal: title.value, bodyLen: (body.innerText || '').length };
39
- })()`,{label:"fill",anchor:Q});if(!J||!J.ok)throw new V(`Hashnode fill failed: ${J?J.reason:"unknown"}`);if(D.draft)return[{status:"draft",draft_id:_,url:Q}];const A=JSON.stringify($),H=q;await z.goto(Q);const L=await W(z,`(async () => {
39
+ })()`,{label:"fill",anchor:Q});if(!J||!J.ok)throw new V(`Hashnode fill failed: ${J?J.reason:"unknown"}`);if(D.draft)return[{status:"draft",draft_id:L,url:Q}];const A=JSON.stringify($),H=q;await z.goto(Q);const N=await W(z,`(async () => {
40
40
  if (location.href.indexOf('/draft/') < 0) return { drift: true };
41
41
  const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
42
42
  let t = 0, tops;
@@ -47,7 +47,7 @@ import*as M from"node:fs";import{cli as I,Strategy as S}from"@jackwener/opencli/
47
47
  tops.forEach((b) => b.removeAttribute('data-pp'));
48
48
  tops[0].setAttribute('data-pp', 'top-publish');
49
49
  return { ok: true };
50
- })()`,{label:"publish-ready",anchor:Q});if(!L||!L.ok)throw new V(`Hashnode publish not ready: ${L?L.reason:"unknown"}`);await z.click('[data-pp="top-publish"]');await z.wait(3);const X=await W(z,`(async () => {
50
+ })()`,{label:"publish-ready",anchor:Q});if(!N||!N.ok)throw new V(`Hashnode publish not ready: ${N?N.reason:"unknown"}`);await z.click('[data-pp="top-publish"]');await z.wait(3);const X=await W(z,`(async () => {
51
51
  if (location.href.indexOf('hashnode.com') < 0) return { drift: true };
52
52
  const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
53
53
  let d = 0, dlg;
@@ -75,7 +75,7 @@ import*as M from"node:fs";import{cli as I,Strategy as S}from"@jackwener/opencli/
75
75
  await sleep(500);
76
76
  }
77
77
  return { ok: true };
78
- })()`,{label:"tags",anchor:Q})}catch(P){}if(H)try{await W(z,`(async () => {
78
+ })()`,{label:"tags",anchor:Q})}catch(T){}if(H)try{await W(z,`(async () => {
79
79
  if (location.href.indexOf('hashnode.com') < 0) return { drift: true };
80
80
  const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
81
81
  var toggle = [...document.querySelectorAll('label,button')].find((e) => /add a canonical url/i.test(e.innerText || ''));
@@ -85,8 +85,8 @@ import*as M from"node:fs";import{cli as I,Strategy as S}from"@jackwener/opencli/
85
85
  var ci = document.querySelector('input[placeholder="https://example.com/original-article"]');
86
86
  if (ci) { var s = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype,'value').set; ci.focus(); s.call(ci, ${JSON.stringify(H)}); ci.dispatchEvent(new Event('input',{bubbles:true})); ci.dispatchEvent(new Event('blur',{bubbles:true})); }
87
87
  return true;
88
- })()`)}catch(P){}await z.evaluate(`(() => {
88
+ })()`)}catch(T){}await z.evaluate(`(() => {
89
89
  var dlg = document.querySelector('[role="dialog"][data-state="open"]') || document.querySelector('[role="dialog"]');
90
90
  if (dlg) { var pub = [...dlg.querySelectorAll('button')].find((b) => (b.innerText||'').trim()==='Publish'); if (pub) pub.setAttribute('data-pp','dlg-publish'); }
91
91
  return true;
92
- })()`);await z.click('[data-pp="dlg-publish"]');let N="";for(let P=0;P<30;P+=1){await z.wait(1);const K=await z.evaluate("(() => location.href)()").catch(()=>"");if(typeof K==="string"&&K.indexOf("/draft/")<0&&K.indexOf("hashnode.com")<0){N=K;break}if(typeof K==="string"&&K.indexOf("/draft/")<0&&K.indexOf("/edit/")<0&&K.indexOf("hashnode.com")>=0){N=K;break}}if(!N)throw new V("Hashnode publish did not navigate to a live article URL (still on /draft or /edit). The post may have been saved but not published.");return[{status:"published",draft_id:_,url:N}]}});
92
+ })()`);await z.click('[data-pp="dlg-publish"]');let P="";for(let T=0;T<30;T+=1){await z.wait(1);const K=await z.evaluate("(() => location.href)()").catch(()=>"");if(typeof K==="string"&&K.indexOf("/draft/")<0&&K.indexOf("hashnode.com")<0){P=K;break}if(typeof K==="string"&&K.indexOf("/draft/")<0&&K.indexOf("/edit/")<0&&K.indexOf("hashnode.com")>=0){P=K;break}}if(!P)throw new V("Hashnode publish did not navigate to a live article URL (still on /draft or /edit). The post may have been saved but not published.");return[{status:"published",draft_id:L,url:P}]}});
@@ -1 +1 @@
1
- import{cli as m,Strategy as s}from"@jackwener/opencli/registry";import{ArgumentError as d}from"@jackwener/opencli/errors";import{WWW_BASE as c,postForm as u,requireOk as a,resolveMomentId as p}from"./utils.js";m({site:"nowcoder",name:"comment",access:"write",description:"评论帖子;配 --to-user/--to-comment 可定向回复某条评论",domain:"www.nowcoder.com",strategy:s.COOKIE,args:[{name:"id",positional:!0,required:!0,help:"帖子 id(数字 momentId / UUID / 链接均可)"},{name:"content",positional:!0,required:!0,help:"评论内容"},{name:"to-user",type:"str",default:"0",help:"帖主/对方 userId(定向回复时必传)"},{name:"to-comment",type:"str",default:"0",help:"定向回复的评论 id(inbox 的 reply_comment_id)"}],columns:["post_id","kind","status"],func:async(n,t)=>{const r=await p(n,t.id),e=String(t.content??"").trim();if(!e)throw new d("评论内容不能为空");const i=String(t["to-user"]??"0").trim()||"0",o=String(t["to-comment"]??"0").trim()||"0";a(await u(n,`${c}/comment/create?_=${Date.now()}`,{entityType:"74",entityId:r,commentContent:e,contentV2:JSON.stringify({pureText:e,imgs:[]}),toFeed:"false",toUserId:i,toCommentId:o,entityOwnerId:i,isAnonymousFlag:"false"}),"评论");return[{post_id:r,kind:o!=="0"?`reply-to-${o}`:"comment",status:"ok"}]}});
1
+ import{cli as m,Strategy as s}from"@jackwener/opencli/registry";import{ArgumentError as d}from"@jackwener/opencli/errors";import{WWW_BASE as c,postForm as u,requireOk as a,resolveMomentId as p}from"./utils.js";import{assertLiteralContent as l}from"../_shared/content-guard.js";m({site:"nowcoder",name:"comment",access:"write",description:"评论帖子;配 --to-user/--to-comment 可定向回复某条评论",domain:"www.nowcoder.com",strategy:s.COOKIE,args:[{name:"id",positional:!0,required:!0,help:"帖子 id(数字 momentId / UUID / 链接均可)"},{name:"content",positional:!0,required:!0,help:"评论内容"},{name:"to-user",type:"str",default:"0",help:"帖主/对方 userId(定向回复时必传)"},{name:"to-comment",type:"str",default:"0",help:"定向回复的评论 id(inbox 的 reply_comment_id)"}],columns:["post_id","kind","status"],func:async(n,t)=>{const r=await p(n,t.id),e=String(t.content??"").trim();l(e,{label:"评论内容"});if(!e)throw new d("评论内容不能为空");const i=String(t["to-user"]??"0").trim()||"0",o=String(t["to-comment"]??"0").trim()||"0";a(await u(n,`${c}/comment/create?_=${Date.now()}`,{entityType:"74",entityId:r,commentContent:e,contentV2:JSON.stringify({pureText:e,imgs:[]}),toFeed:"false",toUserId:i,toCommentId:o,entityOwnerId:i,isAnonymousFlag:"false"}),"评论");return[{post_id:r,kind:o!=="0"?`reply-to-${o}`:"comment",status:"ok"}]}});
@@ -1 +1 @@
1
- import{cli as r,Strategy as i}from"@jackwener/opencli/registry";import{ArgumentError as s}from"@jackwener/opencli/errors";import{GW_BASE as c,postJson as a,requireOk as d}from"./utils.js";r({site:"nowcoder",name:"dm-send",access:"write",description:"发私信(conversation 为 dm-list 的 conversation_id)",domain:"www.nowcoder.com",strategy:i.COOKIE,args:[{name:"conversation",positional:!0,required:!0,help:"会话 id(dm-list 的 conversation_id,格式 <我uid>_<对方uid>)"},{name:"content",positional:!0,required:!0,help:"私信内容"}],columns:["conversation_id","status"],func:async(e,o)=>{const t=String(o.conversation??"").trim(),n=String(o.content??"").trim();if(!n)throw new s("私信内容不能为空");d(await a(e,`${c}/msg/send-msg`,{body:{content:n,sourceType:1200,conversationId:t}}),"私信发送");return[{conversation_id:t,status:"sent"}]}});
1
+ import{cli as r,Strategy as i}from"@jackwener/opencli/registry";import{ArgumentError as s}from"@jackwener/opencli/errors";import{GW_BASE as a,postJson as c,requireOk as d}from"./utils.js";import{assertLiteralContent as m}from"../_shared/content-guard.js";r({site:"nowcoder",name:"dm-send",access:"write",description:"发私信(conversation 为 dm-list 的 conversation_id)",domain:"www.nowcoder.com",strategy:i.COOKIE,args:[{name:"conversation",positional:!0,required:!0,help:"会话 id(dm-list 的 conversation_id,格式 <我uid>_<对方uid>)"},{name:"content",positional:!0,required:!0,help:"私信内容"}],columns:["conversation_id","status"],func:async(n,o)=>{const e=String(o.conversation??"").trim(),t=String(o.content??"").trim();m(t,{label:"私信内容"});if(!t)throw new s("私信内容不能为空");d(await c(n,`${a}/msg/send-msg`,{body:{content:t,sourceType:1200,conversationId:e}}),"私信发送");return[{conversation_id:e,status:"sent"}]}});
@@ -1 +1 @@
1
- import{cli as i,Strategy as c}from"@jackwener/opencli/registry";import{ArgumentError as a,CommandExecutionError as m}from"@jackwener/opencli/errors";import{GW_BASE as l,postJson as s,requireOk as d}from"./utils.js";i({site:"nowcoder",name:"post",access:"write",description:"发布动态(实名发帖,高风险写操作,务必低频且内容有价值)",domain:"www.nowcoder.com",strategy:c.COOKIE,args:[{name:"content",positional:!0,required:!0,help:"正文内容"},{name:"title",type:"str",default:"",help:"标题(可选)"}],columns:["moment_id","content_id","url"],func:async(r,e)=>{const n=String(e.content??"").trim();if(!n)throw new a("发帖正文不能为空");const o=d(await s(r,`${l}/moment/create`,{title:String(e.title??""),content:n,type:0,isAnonymousFlag:!1,publicEntrance:"发动态",publicEntrancePage:"首页"}),"发帖"),t=o.data??{};if(!t.id)throw new m(`牛客发帖响应缺少 momentId: ${JSON.stringify(o).slice(0,200)}`);return[{moment_id:t.id,content_id:t.result??"",url:t.result?`https://www.nowcoder.com/feed/main/detail/${t.result}`:""}]}});
1
+ import{cli as i,Strategy as a}from"@jackwener/opencli/registry";import{ArgumentError as c,CommandExecutionError as m}from"@jackwener/opencli/errors";import{GW_BASE as l,postJson as s,requireOk as d}from"./utils.js";import{assertLiteralContent as p}from"../_shared/content-guard.js";i({site:"nowcoder",name:"post",access:"write",description:"发布动态(实名发帖,高风险写操作,务必低频且内容有价值)",domain:"www.nowcoder.com",strategy:a.COOKIE,args:[{name:"content",positional:!0,required:!0,help:"正文内容"},{name:"title",type:"str",default:"",help:"标题(可选)"}],columns:["moment_id","content_id","url"],func:async(o,n)=>{const e=String(n.content??"").trim();p(e,{label:"正文内容"});if(!e)throw new c("发帖正文不能为空");const r=d(await s(o,`${l}/moment/create`,{title:String(n.title??""),content:e,type:0,isAnonymousFlag:!1,publicEntrance:"发动态",publicEntrancePage:"首页"}),"发帖"),t=r.data??{};if(!t.id)throw new m(`牛客发帖响应缺少 momentId: ${JSON.stringify(r).slice(0,200)}`);return[{moment_id:t.id,content_id:t.result??"",url:t.result?`https://www.nowcoder.com/feed/main/detail/${t.result}`:""}]}});
@@ -1,7 +1,7 @@
1
- import*as $ from"node:fs";import{cli as V,Strategy as v}from"@jackwener/opencli/registry";import{ArgumentError as Q,CommandExecutionError as W}from"@jackwener/opencli/errors";import{qiitaGql as Y,qiitaDeleteByForm as H}from"./gql.js";export function resolveBody(h){if(h["body-file"]){const z=String(h["body-file"]);if(!$.statSync(z,{throwIfNoEntry:!1})?.isFile())throw new Q(`--body-file not found: ${z}`);return $.readFileSync(z,"utf8")}if(h.body!==void 0&&h.body!==null)return String(h.body);return}export function resolveTagNames(h){return String(h??"").split(",").map((z)=>z.trim()).filter(Boolean)}const R=`mutation($input: SaveCreatingArticleInput!) {
1
+ import*as h from"node:fs";import{cli as V,Strategy as v}from"@jackwener/opencli/registry";import{ArgumentError as W,CommandExecutionError as X}from"@jackwener/opencli/errors";import{qiitaGql as Z,qiitaDeleteByForm as H}from"./gql.js";import{assertLiteralContent as R}from"../_shared/content-guard.js";export function resolveBody(z){if(z["body-file"]){const J=String(z["body-file"]);if(!h.statSync(J,{throwIfNoEntry:!1})?.isFile())throw new W(`--body-file not found: ${J}`);return h.readFileSync(J,"utf8")}if(z.body!==void 0&&z.body!==null){R(String(z.body),{fileFlag:"--body-file"});return String(z.body)}return}export function resolveTagNames(z){return String(z??"").split(",").map((J)=>J.trim()).filter(Boolean)}const G=`mutation($input: SaveCreatingArticleInput!) {
2
2
  saveCreatingArticle(input: $input) { draftItem { uuid } }
3
- }`,j=`mutation($input: PublishPublicArticleInput!) {
3
+ }`,I=`mutation($input: PublishPublicArticleInput!) {
4
4
  publishPublicArticle(input: $input) { article { uuid linkUrl encryptedId isSecret } }
5
- }`,G=`mutation($input: PublishSecretArticleInput!) {
5
+ }`,P=`mutation($input: PublishSecretArticleInput!) {
6
6
  publishSecretArticle(input: $input) { article { uuid linkUrl encryptedId isSecret } }
7
- }`;V({site:"qiita",name:"publish",access:"write",description:"Publish a Qiita article (markdown). Tags required. Public by default; --draft or --secret to change.",domain:"qiita.com",strategy:v.COOKIE,browser:!0,args:[{name:"title",type:"string",required:!1,help:"Article title (required)"},{name:"body",type:"string",required:!1,help:"Article body in Markdown (or use --body-file)"},{name:"body-file",type:"string",required:!1,help:"Path to a Markdown file for the body"},{name:"tags",type:"string",required:!1,help:"Comma-separated tags (REQUIRED, at least 1)"},{name:"draft",type:"boolean",required:!1,default:!1,help:"Save as draft only (do not publish)"},{name:"secret",type:"boolean",required:!1,default:!1,help:"Publish as limited-public (secret) instead of public"},{name:"tweet-share",type:"boolean",required:!1,default:!1,help:"Also share to X/Twitter on publish (public only)"}],columns:["status","uuid","url"],func:async(h,z)=>{if(!h)throw new W("Browser session required for qiita publish");const J=String(z.title??"").trim(),K=resolveBody(z),X=resolveTagNames(z.tags);if(!J)throw new Q("--title is required");if(K===void 0)throw new Q("--body or --body-file is required");if(X.length===0)throw new Q("--tags is required (Qiita articles need at least one tag)");await h.goto("https://qiita.com/drafts/new");await h.wait({time:2});const Z=await Y(h,R,{input:{uuid:"",title:J,rawBody:K,tagNames:X,slide:!1,organizationId:null}}),O=Z?.saveCreatingArticle?.draftItem?.uuid;if(!O)throw new W(`Qiita saveCreatingArticle returned no uuid: ${JSON.stringify(Z)}`);if(z.draft)return[{status:"draft",uuid:O,url:`https://qiita.com/drafts/${O}/edit`}];let M;if(z.secret)M=(await Y(h,G,{input:{uuid:O,title:J,rawBody:K,tagNames:X,slide:!1}}))?.publishSecretArticle?.article;else M=(await Y(h,j,{input:{uuid:O,title:J,rawBody:K,tagNames:X,slide:!1,tweetShare:!!z["tweet-share"],adventCalendarItems:[]}}))?.publishPublicArticle?.article;if(!M||!M.linkUrl)throw new W(`Qiita publish returned no article: ${JSON.stringify(M)}`);return[{status:z.secret?"published-secret":"published",uuid:M.uuid||O,url:M.linkUrl}]}});V({site:"qiita",name:"delete",access:"write",description:"Delete a Qiita article or draft by its URL (article linkUrl, or https://qiita.com/drafts/<uuid>).",domain:"qiita.com",strategy:v.COOKIE,browser:!0,args:[{name:"url",type:"string",required:!1,help:"Article URL (e.g. https://qiita.com/<urlName>/items/<uuid>) or draft URL (https://qiita.com/drafts/<uuid>)"}],columns:["status","url"],func:async(h,z)=>{if(!h)throw new W("Browser session required for qiita delete");const J=String(z.url??"").trim();if(!/^https:\/\/qiita\.com\/.+/.test(J))throw new Q("--url is required (a https://qiita.com/... article or draft URL)");await h.goto("https://qiita.com/");await h.wait({time:1});const K=await H(h,J);if(!K.ok)throw new W(`Qiita delete failed (HTTP ${K.status}) for ${J}`);return[{status:"deleted",url:J}]}});
7
+ }`;V({site:"qiita",name:"publish",access:"write",description:"Publish a Qiita article (markdown). Tags required. Public by default; --draft or --secret to change.",domain:"qiita.com",strategy:v.COOKIE,browser:!0,args:[{name:"title",type:"string",required:!1,help:"Article title (required)"},{name:"body",type:"string",required:!1,help:"Article body in Markdown (or use --body-file)"},{name:"body-file",type:"string",required:!1,help:"Path to a Markdown file for the body"},{name:"tags",type:"string",required:!1,help:"Comma-separated tags (REQUIRED, at least 1)"},{name:"draft",type:"boolean",required:!1,default:!1,help:"Save as draft only (do not publish)"},{name:"secret",type:"boolean",required:!1,default:!1,help:"Publish as limited-public (secret) instead of public"},{name:"tweet-share",type:"boolean",required:!1,default:!1,help:"Also share to X/Twitter on publish (public only)"}],columns:["status","uuid","url"],func:async(z,J)=>{if(!z)throw new X("Browser session required for qiita publish");const K=String(J.title??"").trim(),M=resolveBody(J),Y=resolveTagNames(J.tags);if(!K)throw new W("--title is required");if(M===void 0)throw new W("--body or --body-file is required");if(Y.length===0)throw new W("--tags is required (Qiita articles need at least one tag)");await z.goto("https://qiita.com/drafts/new");await z.wait({time:2});const $=await Z(z,G,{input:{uuid:"",title:K,rawBody:M,tagNames:Y,slide:!1,organizationId:null}}),Q=$?.saveCreatingArticle?.draftItem?.uuid;if(!Q)throw new X(`Qiita saveCreatingArticle returned no uuid: ${JSON.stringify($)}`);if(J.draft)return[{status:"draft",uuid:Q,url:`https://qiita.com/drafts/${Q}/edit`}];let O;if(J.secret)O=(await Z(z,P,{input:{uuid:Q,title:K,rawBody:M,tagNames:Y,slide:!1}}))?.publishSecretArticle?.article;else O=(await Z(z,I,{input:{uuid:Q,title:K,rawBody:M,tagNames:Y,slide:!1,tweetShare:!!J["tweet-share"],adventCalendarItems:[]}}))?.publishPublicArticle?.article;if(!O||!O.linkUrl)throw new X(`Qiita publish returned no article: ${JSON.stringify(O)}`);return[{status:J.secret?"published-secret":"published",uuid:O.uuid||Q,url:O.linkUrl}]}});V({site:"qiita",name:"delete",access:"write",description:"Delete a Qiita article or draft by its URL (article linkUrl, or https://qiita.com/drafts/<uuid>).",domain:"qiita.com",strategy:v.COOKIE,browser:!0,args:[{name:"url",type:"string",required:!1,help:"Article URL (e.g. https://qiita.com/<urlName>/items/<uuid>) or draft URL (https://qiita.com/drafts/<uuid>)"}],columns:["status","url"],func:async(z,J)=>{if(!z)throw new X("Browser session required for qiita delete");const K=String(J.url??"").trim();if(!/^https:\/\/qiita\.com\/.+/.test(K))throw new W("--url is required (a https://qiita.com/... article or draft URL)");await z.goto("https://qiita.com/");await z.wait({time:1});const M=await H(z,K);if(!M.ok)throw new X(`Qiita delete failed (HTTP ${M.status}) for ${K}`);return[{status:"deleted",url:K}]}});
@@ -1,4 +1,4 @@
1
- import*as _ from"node:fs";import{cli as $,Strategy as M}from"@jackwener/opencli/registry";import{ArgumentError as Q,AuthRequiredError as W,CommandExecutionError as N}from"@jackwener/opencli/errors";const F="https://www.tumblr.com",j="https://www.tumblr.com/dashboard",X=["published","draft","queue","private"];export function blogHost(z){const G=String(z||"").trim();if(!G)return"";return G.includes(".")?G:`${G}.tumblr.com`}export function resolveBody(z){if(z["body-file"]){const G=String(z["body-file"]);if(!_.statSync(G,{throwIfNoEntry:!1})?.isFile())throw new Q(`--body-file not found: ${G}`);return _.readFileSync(G,"utf8")}if(z.body!==void 0&&z.body!==null)return String(z.body);if(z.text!==void 0&&z.text!==null)return String(z.text);return}export function buildContent(z,G){const I=[];if(z.title)I.push({type:"text",subtype:"heading1",text:String(z.title)});if(G!==void 0&&G!=="")for(const L of String(G).split(/\n{2,}/)){const J=L.replace(/\n+$/,"");if(J.trim()!=="")I.push({type:"text",text:J})}if(z["image-url"])I.push({type:"image",media:[{url:String(z["image-url"])}]});if(z.link)I.push({type:"link",url:String(z.link)});if(I.length===0)throw new Q("Provide at least one of --title, --text/--body/--body-file, --image-url, or --link");return I}export function resolveTags(z){if(z===void 0||z===null||z==="")return"";return String(z).split(",").map((G)=>G.trim()).filter(Boolean).join(",")}async function Y(z,G,I){return z.evaluate(`(async () => {
1
+ import*as M from"node:fs";import{cli as _,Strategy as L}from"@jackwener/opencli/registry";import{ArgumentError as U,AuthRequiredError as X,CommandExecutionError as P}from"@jackwener/opencli/errors";import{assertLiteralContent as j}from"../_shared/content-guard.js";const O="https://www.tumblr.com",q="https://www.tumblr.com/dashboard",Y=["published","draft","queue","private"];export function blogHost(z){const G=String(z||"").trim();if(!G)return"";return G.includes(".")?G:`${G}.tumblr.com`}export function resolveBody(z){if(z["body-file"]){const G=String(z["body-file"]);if(!M.statSync(G,{throwIfNoEntry:!1})?.isFile())throw new U(`--body-file not found: ${G}`);return M.readFileSync(G,"utf8")}if(z.body!==void 0&&z.body!==null){j(String(z.body),{fileFlag:"--body-file"});return String(z.body)}if(z.text!==void 0&&z.text!==null){j(String(z.text),{fileFlag:"--body-file"});return String(z.text)}return}export function buildContent(z,G){const I=[];if(z.title)I.push({type:"text",subtype:"heading1",text:String(z.title)});if(G!==void 0&&G!=="")for(const N of String(G).split(/\n{2,}/)){const J=N.replace(/\n+$/,"");if(J.trim()!=="")I.push({type:"text",text:J})}if(z["image-url"])I.push({type:"image",media:[{url:String(z["image-url"])}]});if(z.link)I.push({type:"link",url:String(z.link)});if(I.length===0)throw new U("Provide at least one of --title, --text/--body/--body-file, --image-url, or --link");return I}export function resolveTags(z){if(z===void 0||z===null||z==="")return"";return String(z).split(",").map((G)=>G.trim()).filter(Boolean).join(",")}async function Z(z,G,I){return z.evaluate(`(async () => {
2
2
  try {
3
3
  if (!(window.tumblr && typeof window.tumblr.apiFetch === 'function')) {
4
4
  return { kind: 'auth', detail: 'apiFetch unavailable — not on a logged-in tumblr page?' };
@@ -11,4 +11,4 @@ import*as _ from"node:fs";import{cli as $,Strategy as M}from"@jackwener/opencli/
11
11
  if (status === 401 || status === 403) return { kind: 'auth', detail: 'HTTP ' + status + ' ' + msg };
12
12
  return { kind: 'http', status: status, detail: String(msg).slice(0, 300) };
13
13
  }
14
- })()`)}async function q(z,G){if(G.blog)return blogHost(G.blog);const I=await Y(z,"/v2/user/info",{method:"GET"});if(I?.kind==="auth")throw new W("tumblr.com",`Not logged in (${I.detail}). Run \`opencli tumblr login\`.`);if(I?.kind!=="ok"||!I.response||!I.response.user)throw new N(`Failed to resolve default blog: ${JSON.stringify(I)}`);const L=I.response.user.blogs||[],J=L.find((P)=>P.primary)||L[0];if(!J||!J.name)throw new N("No blog found on this account; pass --blog explicitly.");return blogHost(J.name)}$({site:"tumblr",name:"publish",access:"write",description:"Publish a Tumblr post (NPF: title/text/image/link). Draft by default; pass --state published to go live.",domain:"tumblr.com",strategy:M.COOKIE,browser:!0,args:[{name:"title",type:"string",required:!1,help:"Title (rendered as a heading1 text block)"},{name:"text",type:"string",required:!1,help:"Body text (blank lines split paragraphs); alias of --body"},{name:"body",type:"string",required:!1,help:"Body text (blank lines split paragraphs)"},{name:"body-file",type:"string",required:!1,help:"Path to a text file for the body"},{name:"image-url",type:"string",required:!1,help:"Image URL (NPF image block)"},{name:"link",type:"string",required:!1,help:"Link URL (NPF link block)"},{name:"tags",type:"string",required:!1,help:"Comma-separated tags"},{name:"state",type:"string",required:!1,default:"draft",choices:X,help:"draft | published | queue | private (default: draft)"},{name:"blog",type:"string",required:!1,help:"Target blog identifier (else your primary blog)"}],columns:["status","id","blog","state","url"],func:async(z,G)=>{if(!z)throw new N("Browser session required for tumblr publish");const I=String(G.state??"draft");if(!X.includes(I))throw new Q(`Invalid --state "${I}". One of: ${X.join(", ")}`);const L=resolveBody(G),J=buildContent(G,L),P=resolveTags(G.tags);await z.goto(j);const U=await q(z,G),Z={content:J,state:I};if(P)Z.tags=P;const K=await Y(z,`/v2/blog/${U}/posts`,{method:"POST",body:Z});if(K?.kind==="auth")throw new W("tumblr.com",`Not logged in (${K.detail}). Run \`opencli tumblr login\`.`);if(K?.kind==="http")throw new N(`Tumblr publish failed: HTTP ${K.status} ${K.detail}`);if(K?.kind!=="ok"||!K.response)throw new N(`Unexpected Tumblr response: ${JSON.stringify(K)}`);const V=K.response.id_string??(K.response.id!=null?String(K.response.id):"");if(!V)throw new N("Tumblr post response missing id");return[{status:"success",id:V,blog:U,state:I,url:I==="published"?`${F}/${U}/${V}`:""}]}});$({site:"tumblr",name:"delete",access:"write",description:"Delete a Tumblr post by id (POST /v2/blog/{blog}/post/delete).",domain:"tumblr.com",strategy:M.COOKIE,browser:!0,args:[{name:"id",type:"string",required:!0,help:"Post id to delete"},{name:"blog",type:"string",required:!1,help:"Target blog identifier (else your primary blog)"}],columns:["status","id","blog"],func:async(z,G)=>{if(!z)throw new N("Browser session required for tumblr delete");const I=String(G.id??"").trim();if(!I)throw new Q("--id is required");await z.goto(j);const L=await q(z,G),J=await Y(z,`/v2/blog/${L}/post/delete`,{method:"POST",body:{id:I}});if(J?.kind==="auth")throw new W("tumblr.com",`Not logged in (${J.detail}). Run \`opencli tumblr login\`.`);if(J?.kind==="http")throw new N(`Tumblr delete failed: HTTP ${J.status} ${J.detail}`);if(J?.kind!=="ok")throw new N(`Unexpected Tumblr delete response: ${JSON.stringify(J)}`);return[{status:"deleted",id:I,blog:L}]}});export const __test__={blogHost,resolveBody,buildContent,resolveTags};
14
+ })()`)}async function F(z,G){if(G.blog)return blogHost(G.blog);const I=await Z(z,"/v2/user/info",{method:"GET"});if(I?.kind==="auth")throw new X("tumblr.com",`Not logged in (${I.detail}). Run \`opencli tumblr login\`.`);if(I?.kind!=="ok"||!I.response||!I.response.user)throw new P(`Failed to resolve default blog: ${JSON.stringify(I)}`);const N=I.response.user.blogs||[],J=N.find((Q)=>Q.primary)||N[0];if(!J||!J.name)throw new P("No blog found on this account; pass --blog explicitly.");return blogHost(J.name)}_({site:"tumblr",name:"publish",access:"write",description:"Publish a Tumblr post (NPF: title/text/image/link). Draft by default; pass --state published to go live.",domain:"tumblr.com",strategy:L.COOKIE,browser:!0,args:[{name:"title",type:"string",required:!1,help:"Title (rendered as a heading1 text block)"},{name:"text",type:"string",required:!1,help:"Body text (blank lines split paragraphs); alias of --body"},{name:"body",type:"string",required:!1,help:"Body text (blank lines split paragraphs)"},{name:"body-file",type:"string",required:!1,help:"Path to a text file for the body"},{name:"image-url",type:"string",required:!1,help:"Image URL (NPF image block)"},{name:"link",type:"string",required:!1,help:"Link URL (NPF link block)"},{name:"tags",type:"string",required:!1,help:"Comma-separated tags"},{name:"state",type:"string",required:!1,default:"draft",choices:Y,help:"draft | published | queue | private (default: draft)"},{name:"blog",type:"string",required:!1,help:"Target blog identifier (else your primary blog)"}],columns:["status","id","blog","state","url"],func:async(z,G)=>{if(!z)throw new P("Browser session required for tumblr publish");const I=String(G.state??"draft");if(!Y.includes(I))throw new U(`Invalid --state "${I}". One of: ${Y.join(", ")}`);const N=resolveBody(G),J=buildContent(G,N),Q=resolveTags(G.tags);await z.goto(q);const V=await F(z,G),$={content:J,state:I};if(Q)$.tags=Q;const K=await Z(z,`/v2/blog/${V}/posts`,{method:"POST",body:$});if(K?.kind==="auth")throw new X("tumblr.com",`Not logged in (${K.detail}). Run \`opencli tumblr login\`.`);if(K?.kind==="http")throw new P(`Tumblr publish failed: HTTP ${K.status} ${K.detail}`);if(K?.kind!=="ok"||!K.response)throw new P(`Unexpected Tumblr response: ${JSON.stringify(K)}`);const W=K.response.id_string??(K.response.id!=null?String(K.response.id):"");if(!W)throw new P("Tumblr post response missing id");return[{status:"success",id:W,blog:V,state:I,url:I==="published"?`${O}/${V}/${W}`:""}]}});_({site:"tumblr",name:"delete",access:"write",description:"Delete a Tumblr post by id (POST /v2/blog/{blog}/post/delete).",domain:"tumblr.com",strategy:L.COOKIE,browser:!0,args:[{name:"id",type:"string",required:!0,help:"Post id to delete"},{name:"blog",type:"string",required:!1,help:"Target blog identifier (else your primary blog)"}],columns:["status","id","blog"],func:async(z,G)=>{if(!z)throw new P("Browser session required for tumblr delete");const I=String(G.id??"").trim();if(!I)throw new U("--id is required");await z.goto(q);const N=await F(z,G),J=await Z(z,`/v2/blog/${N}/post/delete`,{method:"POST",body:{id:I}});if(J?.kind==="auth")throw new X("tumblr.com",`Not logged in (${J.detail}). Run \`opencli tumblr login\`.`);if(J?.kind==="http")throw new P(`Tumblr delete failed: HTTP ${J.status} ${J.detail}`);if(J?.kind!=="ok")throw new P(`Unexpected Tumblr delete response: ${JSON.stringify(J)}`);return[{status:"deleted",id:I,blog:N}]}});export const __test__={blogHost,resolveBody,buildContent,resolveTags};
@@ -1,7 +1,7 @@
1
- import{cli as Z,Strategy as $}from"@jackwener/opencli/registry";import{ArgumentError as q,CommandExecutionError as V}from"@jackwener/opencli/errors";import{DM_SEND_QUERY as N,buildQuery as P,getDmAuth as R,resolveTwitterUserId as T,shapeDmMessages as Y}from"./dm-shared.js";Z({site:"twitter",name:"dm-send",access:"write",description:"Send a direct message to one user (by @screen_name, numeric user id, or conversation id from dm-list)",domain:"x.com",strategy:$.COOKIE,browser:!0,args:[{name:"user",type:"string",required:!0,positional:!0,help:"Recipient: @screen_name, numeric user id, or a conversation id like 123-456 from `dm-list`"},{name:"text",type:"string",required:!0,positional:!0,help:"Message text to send"},{name:"reply-to",type:"string",required:!1,help:"Message id to reply to (from `dm-read`)"}],columns:["id","conversation_id","text","time","status"],func:async(G,B)=>{const J=String(B.text??"").trim();if(!J)throw new q("Message text must not be empty");const{ownId:W,headers:K}=await R(G),H=String(B.user??"").trim();let F;if(/^\d+-\d+$/.test(H))F=H;else F=`${await T(G,H,K)}-${W}`;const L={cards_platform:"Web-12",conversation_id:F,dm_users:!1,include_cards:1,include_quote_count:!0,recipient_ids:!1,text:J,audio_only_media_attachment:!1};if(B["reply-to"])L.reply_to_dm_id=String(B["reply-to"]).trim();const X="https://x.com/i/api/1.1/dm/new2.json?"+P(N),f=await G.evaluate(`async () => {
2
- const body = ${JSON.stringify(L)};
1
+ import{cli as $,Strategy as q}from"@jackwener/opencli/registry";import{ArgumentError as L,CommandExecutionError as W}from"@jackwener/opencli/errors";import{DM_SEND_QUERY as N,buildQuery as P,getDmAuth as R,resolveTwitterUserId as T,shapeDmMessages as Y}from"./dm-shared.js";import{assertLiteralContent as A}from"../_shared/content-guard.js";$({site:"twitter",name:"dm-send",access:"write",description:"Send a direct message to one user (by @screen_name, numeric user id, or conversation id from dm-list)",domain:"x.com",strategy:q.COOKIE,browser:!0,args:[{name:"user",type:"string",required:!0,positional:!0,help:"Recipient: @screen_name, numeric user id, or a conversation id like 123-456 from `dm-list`"},{name:"text",type:"string",required:!0,positional:!0,help:"Message text to send"},{name:"reply-to",type:"string",required:!1,help:"Message id to reply to (from `dm-read`)"}],columns:["id","conversation_id","text","time","status"],func:async(G,B)=>{const H=String(B.text??"").trim();A(H,{label:"私信内容"});if(!H)throw new L("Message text must not be empty");const{ownId:X,headers:K}=await R(G),J=String(B.user??"").trim();let F;if(/^\d+-\d+$/.test(J))F=J;else F=`${await T(G,J,K)}-${X}`;const O={cards_platform:"Web-12",conversation_id:F,dm_users:!1,include_cards:1,include_quote_count:!0,recipient_ids:!1,text:H,audio_only_media_attachment:!1};if(B["reply-to"])O.reply_to_dm_id=String(B["reply-to"]).trim();const Z="https://x.com/i/api/1.1/dm/new2.json?"+P(N),f=await G.evaluate(`async () => {
2
+ const body = ${JSON.stringify(O)};
3
3
  body.request_id = crypto.randomUUID();
4
- const r = await fetch(${JSON.stringify(X)}, {
4
+ const r = await fetch(${JSON.stringify(Z)}, {
5
5
  method: 'POST',
6
6
  headers: Object.assign(${JSON.stringify(K)}, { 'Content-Type': 'application/json' }),
7
7
  credentials: 'include',
@@ -10,4 +10,4 @@ import{cli as Z,Strategy as $}from"@jackwener/opencli/registry";import{ArgumentE
10
10
  const text = await r.text();
11
11
  if (!r.ok) return { error: r.status, body: text.slice(0, 300) };
12
12
  try { return JSON.parse(text); } catch { return { error: 'bad-json', body: text.slice(0, 300) }; }
13
- }`);if(f?.error)throw new V(`dm/new2.json failed: HTTP ${f.error} ${f.body||""}`);const O=Y(f.entries,f.users);if(!O.length)throw new V(`DM send returned no message entry (response keys: ${Object.keys(f||{}).join(",")})`);return O.map((z)=>({id:z.id,conversation_id:z.conversation_id||F,text:z.text,time:z.time,status:"sent"}))}});
13
+ }`);if(f?.error)throw new W(`dm/new2.json failed: HTTP ${f.error} ${f.body||""}`);const V=Y(f.entries,f.users);if(!V.length)throw new W(`DM send returned no message entry (response keys: ${Object.keys(f||{}).join(",")})`);return V.map((z)=>({id:z.id,conversation_id:z.conversation_id||F,text:z.text,time:z.time,status:"sent"}))}});
@@ -1,11 +1,11 @@
1
- import*as B from"node:fs";import*as X from"node:path";import{cli as w,Strategy as U}from"@jackwener/opencli/registry";import{AuthRequiredError as D,CommandExecutionError as G}from"@jackwener/opencli/errors";import{describeTwitterApiError as m,resolveTwitterOperationMetadata as L,unwrapBrowserResult as I}from"./shared.js";import{isRecoverableFileInputError as b,TWITTER_BEARER_TOKEN as c}from"./utils.js";const N=4,k=500,F=30000,y=250,M=1e4,j=500,A=15000,S="https://x.com/compose/post",x="https://x.com/home",J='input[type="file"][data-testid="fileInput"]',O=new Set([".jpg",".jpeg",".png",".gif",".webp"]),d={queryId:"5CdvsV_zjv4L64XFifAglw",features:{premium_content_api_read_enabled:!1,communities_web_enable_tweet_community_results_fetch:!0,c9s_tweet_anatomy_moderator_badge_enabled:!0,responsive_web_grok_analyze_button_fetch_trends_enabled:!1,responsive_web_grok_analyze_post_followups_enabled:!0,rweb_cashtags_composer_attachment_enabled:!0,responsive_web_jetfuel_frame:!0,responsive_web_grok_share_attachment_enabled:!0,responsive_web_grok_annotations_enabled:!0,responsive_web_edit_tweet_api_enabled:!0,rweb_conversational_replies_downvote_enabled:!1,graphql_is_translatable_rweb_tweet_is_translatable_enabled:!0,view_counts_everywhere_api_enabled:!0,longform_notetweets_consumption_enabled:!0,responsive_web_twitter_article_tweet_consumption_enabled:!0,content_disclosure_indicator_enabled:!0,content_disclosure_ai_generated_indicator_enabled:!0,responsive_web_grok_show_grok_translated_post:!0,responsive_web_grok_analysis_button_from_backend:!0,post_ctas_fetch_enabled:!0,longform_notetweets_rich_text_read_enabled:!0,longform_notetweets_inline_media_enabled:!1,profile_label_improvements_pcf_label_in_post_enabled:!0,responsive_web_profile_redirect_enabled:!1,rweb_tipjar_consumption_enabled:!1,verified_phone_label_enabled:!1,articles_preview_enabled:!0,rweb_cashtags_enabled:!0,responsive_web_grok_community_note_auto_translation_is_enabled:!0,responsive_web_graphql_skip_user_profile_image_extensions_enabled:!1,freedom_of_speech_not_reach_fetch_enabled:!0,standardized_nudges_misinfo:!0,tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled:!0,responsive_web_grok_image_annotation_enabled:!0,responsive_web_grok_imagine_annotation_enabled:!0,responsive_web_graphql_timeline_navigation_enabled:!0},fieldToggles:{}};function h(z){const Q=z.split(",").map((H)=>H.trim()).filter(Boolean);if(Q.length>N)throw new G(`Too many images: ${Q.length} (max ${N})`);return Q.map((H)=>{const Y=X.resolve(H),$=X.extname(Y).toLowerCase();if(!O.has($))throw new G(`Unsupported image format "${$}". Supported: jpg, png, gif, webp`);const Z=B.statSync(Y,{throwIfNoEntry:!1});if(!Z||!Z.isFile())throw new G(`Not a valid file: ${Y}`);return Y})}function _(z){const H=(z instanceof Error?z.message:String(z)).toLowerCase();return H.includes("unknown action")||H.includes("not supported")||H.includes("inserttext returned no inserted flag")}async function T(z){return z.evaluate(`(() => {
1
+ import*as B from"node:fs";import*as X from"node:path";import{cli as w,Strategy as U}from"@jackwener/opencli/registry";import{AuthRequiredError as D,CommandExecutionError as G}from"@jackwener/opencli/errors";import{describeTwitterApiError as m,resolveTwitterOperationMetadata as L,unwrapBrowserResult as I}from"./shared.js";import{isRecoverableFileInputError as b,TWITTER_BEARER_TOKEN as M}from"./utils.js";import{assertLiteralContent as S}from"../_shared/content-guard.js";const N=4,k=500,j=30000,y=250,c=1e4,F=500,A=15000,x="https://x.com/compose/post",O="https://x.com/home",J='input[type="file"][data-testid="fileInput"]',h=new Set([".jpg",".jpeg",".png",".gif",".webp"]),d={queryId:"5CdvsV_zjv4L64XFifAglw",features:{premium_content_api_read_enabled:!1,communities_web_enable_tweet_community_results_fetch:!0,c9s_tweet_anatomy_moderator_badge_enabled:!0,responsive_web_grok_analyze_button_fetch_trends_enabled:!1,responsive_web_grok_analyze_post_followups_enabled:!0,rweb_cashtags_composer_attachment_enabled:!0,responsive_web_jetfuel_frame:!0,responsive_web_grok_share_attachment_enabled:!0,responsive_web_grok_annotations_enabled:!0,responsive_web_edit_tweet_api_enabled:!0,rweb_conversational_replies_downvote_enabled:!1,graphql_is_translatable_rweb_tweet_is_translatable_enabled:!0,view_counts_everywhere_api_enabled:!0,longform_notetweets_consumption_enabled:!0,responsive_web_twitter_article_tweet_consumption_enabled:!0,content_disclosure_indicator_enabled:!0,content_disclosure_ai_generated_indicator_enabled:!0,responsive_web_grok_show_grok_translated_post:!0,responsive_web_grok_analysis_button_from_backend:!0,post_ctas_fetch_enabled:!0,longform_notetweets_rich_text_read_enabled:!0,longform_notetweets_inline_media_enabled:!1,profile_label_improvements_pcf_label_in_post_enabled:!0,responsive_web_profile_redirect_enabled:!1,rweb_tipjar_consumption_enabled:!1,verified_phone_label_enabled:!1,articles_preview_enabled:!0,rweb_cashtags_enabled:!0,responsive_web_grok_community_note_auto_translation_is_enabled:!0,responsive_web_graphql_skip_user_profile_image_extensions_enabled:!1,freedom_of_speech_not_reach_fetch_enabled:!0,standardized_nudges_misinfo:!0,tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled:!0,responsive_web_grok_image_annotation_enabled:!0,responsive_web_grok_imagine_annotation_enabled:!0,responsive_web_graphql_timeline_navigation_enabled:!0},fieldToggles:{}};function _(z){const Q=z.split(",").map((H)=>H.trim()).filter(Boolean);if(Q.length>N)throw new G(`Too many images: ${Q.length} (max ${N})`);return Q.map((H)=>{const Y=X.resolve(H),$=X.extname(Y).toLowerCase();if(!h.has($))throw new G(`Unsupported image format "${$}". Supported: jpg, png, gif, webp`);const Z=B.statSync(Y,{throwIfNoEntry:!1});if(!Z||!Z.isFile())throw new G(`Not a valid file: ${Y}`);return Y})}function T(z){const H=(z instanceof Error?z.message:String(z)).toLowerCase();return H.includes("unknown action")||H.includes("not supported")||H.includes("inserttext returned no inserted flag")}async function P(z){return z.evaluate(`(() => {
2
2
  const visible = (el) => !!el && (el.offsetParent !== null || el.getClientRects().length > 0);
3
3
  const boxes = Array.from(document.querySelectorAll('[data-testid="tweetTextarea_0"]'));
4
4
  const box = boxes.find(visible) || boxes[0];
5
5
  if (!box) return { ok: false, message: 'Could not find the tweet composer text area. Are you logged in?' };
6
6
  box.focus();
7
7
  return { ok: true };
8
- })()`)}async function P(z,Q){const H=Math.ceil(M/y);return z.evaluate(`(async () => {
8
+ })()`)}async function E(z,Q){const H=Math.ceil(c/y);return z.evaluate(`(async () => {
9
9
  const expected = ${JSON.stringify(Q)};
10
10
  const normalize = s => String(s || '').replace(/ /g, ' ').replace(/s+/g, ' ').trim();
11
11
  const normalizedExpected = normalize(expected);
@@ -21,7 +21,7 @@ import*as B from"node:fs";import*as X from"node:path";import{cli as w,Strategy a
21
21
  message: 'Could not verify tweet text in the composer after typing.',
22
22
  actualText: box ? (box.innerText || box.textContent || '') : ''
23
23
  };
24
- })()`)}async function n(z,Q){const H=await T(z);if(!H?.ok)return H;const Y=[z.nativeType?.bind(z),z.insertText?.bind(z)].filter(Boolean);for(const $ of Y)try{await $(Q);const Z=await P(z,Q);if(Z?.ok)return Z}catch(Z){if(!_(Z))throw Z}return z.evaluate(`(async () => {
24
+ })()`)}async function n(z,Q){const H=await P(z);if(!H?.ok)return H;const Y=[z.nativeType?.bind(z),z.insertText?.bind(z)].filter(Boolean);for(const $ of Y)try{await $(Q);const Z=await E(z,Q);if(Z?.ok)return Z}catch(Z){if(!T(Z))throw Z}return z.evaluate(`(async () => {
25
25
  try {
26
26
  const visible = (el) => !!el && (el.offsetParent !== null || el.getClientRects().length > 0);
27
27
  const boxes = Array.from(document.querySelectorAll('[data-testid="tweetTextarea_0"]'));
@@ -40,7 +40,7 @@ import*as B from"node:fs";import*as X from"node:path";import{cli as w,Strategy a
40
40
  if (normalize(actual).includes(normalize(textToInsert))) return { ok: true };
41
41
  return { ok: false, message: 'Could not verify tweet text in the composer after typing.', actualText: actual };
42
42
  } catch (e) { return { ok: false, message: String(e) }; }
43
- })()`)}async function E(z,Q){const H=Math.ceil(F/k);return z.evaluate(`(async () => {
43
+ })()`)}async function u(z,Q){const H=Math.ceil(j/k);return z.evaluate(`(async () => {
44
44
  const expected = ${JSON.stringify(Q)};
45
45
  const visible = (el) => !!el && (el.offsetParent !== null || el.getClientRects().length > 0);
46
46
  for (let i = 0; i < ${JSON.stringify(H)}; i++) {
@@ -58,7 +58,7 @@ import*as B from"node:fs";import*as X from"node:path";import{cli as w,Strategy a
58
58
  const buttonReady = !!button && !button.disabled && button.getAttribute('aria-disabled') !== 'true';
59
59
  if (previewCount >= expected && buttonReady) return { ok: true, previewCount };
60
60
  }
61
- return { ok: false, message: 'Image upload timed out (${F/1000}s).' };
61
+ return { ok: false, message: 'Image upload timed out (${j/1000}s).' };
62
62
  })()`)}async function f(z,Q){const H=Q.map(($)=>{const Z=X.extname($).toLowerCase(),W=Z===".png"?"image/png":Z===".gif"?"image/gif":Z===".webp"?"image/webp":"image/jpeg";return{name:X.basename($),mime:W,base64:B.readFileSync($).toString("base64")}}),Y=await z.evaluate(`(() => {
63
63
  const input = document.querySelector(${JSON.stringify(J)});
64
64
  if (!input) return { ok: false, error: 'No file input found' };
@@ -86,7 +86,7 @@ import*as B from"node:fs";import*as X from"node:path";import{cli as w,Strategy a
86
86
  input.dispatchEvent(new Event('change', { bubbles: true }));
87
87
  input.dispatchEvent(new Event('input', { bubbles: true }));
88
88
  return { ok: true };
89
- })()`);if(!Y?.ok)throw new G(`Image upload failed (base64 fallback): ${Y?.error??"unknown error"}`)}async function u(z,Q){const H=Math.ceil(A/j),Y=await z.evaluate(`(async () => {
89
+ })()`);if(!Y?.ok)throw new G(`Image upload failed (base64 fallback): ${Y?.error??"unknown error"}`)}async function l(z,Q){const H=Math.ceil(A/F),Y=await z.evaluate(`(async () => {
90
90
  try {
91
91
  const visible = (el) => !!el && (el.offsetParent !== null || el.getClientRects().length > 0);
92
92
  // For text-only posts the Tweet button only becomes enabled once
@@ -99,13 +99,13 @@ import*as B from"node:fs";import*as X from"node:path";import{cli as w,Strategy a
99
99
  const buttons = Array.from(document.querySelectorAll('[data-testid="tweetButtonInline"], [data-testid="tweetButton"]'));
100
100
  btn = buttons.find((el) => visible(el) && !el.disabled && el.getAttribute('aria-disabled') !== 'true');
101
101
  if (btn) break;
102
- await new Promise(r => setTimeout(r, ${JSON.stringify(j)}));
102
+ await new Promise(r => setTimeout(r, ${JSON.stringify(F)}));
103
103
  }
104
104
  if (!btn) return { ok: false, message: 'Tweet button is disabled or not found.' };
105
105
  btn.click();
106
106
  return { ok: true };
107
107
  } catch (e) { return { ok: false, message: String(e) }; }
108
- })()`);if(!Y?.ok)return Y;const $=Math.ceil(A/j);return z.evaluate(`(async () => {
108
+ })()`);if(!Y?.ok)return Y;const $=Math.ceil(A/F);return z.evaluate(`(async () => {
109
109
  const expected = ${JSON.stringify(Q)};
110
110
  const normalize = s => String(s || '').replace(/ /g, ' ').replace(/s+/g, ' ').trim();
111
111
  const expectedText = normalize(expected);
@@ -124,7 +124,7 @@ import*as B from"node:fs";import*as X from"node:path";import{cli as w,Strategy a
124
124
  return {};
125
125
  };
126
126
  for (let i = 0; i < ${JSON.stringify($)}; i++) {
127
- await new Promise(r => setTimeout(r, ${JSON.stringify(j)}));
127
+ await new Promise(r => setTimeout(r, ${JSON.stringify(F)}));
128
128
  const toasts = Array.from(document.querySelectorAll('[role="alert"], [data-testid="toast"]'))
129
129
  .filter((el) => visible(el));
130
130
  const successToast = toasts.find((el) => /sent|posted|your post was sent|your tweet was sent/i.test(el.textContent || ''));
@@ -144,7 +144,7 @@ import*as B from"node:fs";import*as X from"node:path";import{cli as w,Strategy a
144
144
  }
145
145
  }
146
146
  return { ok: false, message: 'Tweet submission did not complete before timeout.' };
147
- })()`)}function C(z){const Q=Array.isArray(z?.errors)?z.errors:[];if(Q.length===0)return"";return Q.map((H)=>H?.message||JSON.stringify(H)).filter(Boolean).join("; ").slice(0,300)}function l(z){const Q=z?.data?.create_tweet?.tweet_results?.result,H=Q?.tweet||Q;if(!H||typeof H!=="object")return null;const Y=String(H.rest_id||H.legacy?.id_str||"").trim();if(!/^\d+$/.test(Y))return null;const $=H.core?.user_results?.result,Z=String($?.core?.screen_name||$?.legacy?.screen_name||"").trim();return{id:Y,url:Z?`https://x.com/${Z}/status/${Y}`:`https://x.com/i/status/${Y}`}}async function i(z,Q){await z.goto(x,{waitUntil:"load",settleMs:1000});const Y=(await z.getCookies({url:"https://x.com"})).find((q)=>q.name==="ct0")?.value||null;if(!Y)throw new D("x.com","Not logged into x.com (no ct0 cookie)");const $=await L(z,"CreateTweet",d),Z={variables:{tweet_text:Q,dark_request:!1,media:{media_entities:[],possibly_sensitive:!1},semantic_annotation_ids:[]},features:$.features,queryId:$.queryId};if($.fieldToggles&&Object.keys($.fieldToggles).length>0)Z.fieldToggles=$.fieldToggles;const W=JSON.stringify({Authorization:`Bearer ${decodeURIComponent(c)}`,"X-Csrf-Token":Y,"X-Twitter-Auth-Type":"OAuth2Session","X-Twitter-Active-User":"yes","Content-Type":"application/json"}),K=`/i/api/graphql/${$.queryId}/CreateTweet`,R=JSON.stringify(Z),V=I(await z.evaluate(`async () => {
147
+ })()`)}function C(z){const Q=Array.isArray(z?.errors)?z.errors:[];if(Q.length===0)return"";return Q.map((H)=>H?.message||JSON.stringify(H)).filter(Boolean).join("; ").slice(0,300)}function i(z){const Q=z?.data?.create_tweet?.tweet_results?.result,H=Q?.tweet||Q;if(!H||typeof H!=="object")return null;const Y=String(H.rest_id||H.legacy?.id_str||"").trim();if(!/^\d+$/.test(Y))return null;const $=H.core?.user_results?.result,Z=String($?.core?.screen_name||$?.legacy?.screen_name||"").trim();return{id:Y,url:Z?`https://x.com/${Z}/status/${Y}`:`https://x.com/i/status/${Y}`}}async function g(z,Q){await z.goto(O,{waitUntil:"load",settleMs:1000});const Y=(await z.getCookies({url:"https://x.com"})).find((q)=>q.name==="ct0")?.value||null;if(!Y)throw new D("x.com","Not logged into x.com (no ct0 cookie)");const $=await L(z,"CreateTweet",d),Z={variables:{tweet_text:Q,dark_request:!1,media:{media_entities:[],possibly_sensitive:!1},semantic_annotation_ids:[]},features:$.features,queryId:$.queryId};if($.fieldToggles&&Object.keys($.fieldToggles).length>0)Z.fieldToggles=$.fieldToggles;const W=JSON.stringify({Authorization:`Bearer ${decodeURIComponent(M)}`,"X-Csrf-Token":Y,"X-Twitter-Auth-Type":"OAuth2Session","X-Twitter-Active-User":"yes","Content-Type":"application/json"}),K=`/i/api/graphql/${$.queryId}/CreateTweet`,R=JSON.stringify(Z),V=I(await z.evaluate(`async () => {
148
148
  const r = await fetch(${JSON.stringify(K)}, {
149
149
  method: 'POST',
150
150
  headers: ${W},
@@ -155,4 +155,4 @@ import*as B from"node:fs";import*as X from"node:path";import{cli as w,Strategy a
155
155
  let bodyJson = null;
156
156
  try { bodyJson = JSON.parse(bodyText); } catch {}
157
157
  return { ok: r.ok, httpStatus: r.status, bodyJson, bodyText };
158
- }`));if(V?.httpStatus===401||V?.httpStatus===403)throw new D("x.com",`Twitter CreateTweet returned HTTP ${V.httpStatus}`);if(!V?.ok){const q=C(V?.bodyJson);return{ok:!1,message:m("CreateTweet",V?.httpStatus??0,q||void 0)}}const v=l(V.bodyJson);if(!v){const q=C(V.bodyJson);return{ok:!1,message:q?`CreateTweet failed: ${q}`:"CreateTweet returned no created tweet id."}}return{ok:!0,message:"Tweet posted successfully.",...v}}w({site:"twitter",name:"post",access:"write",description:"Post a new tweet/thread",domain:"x.com",strategy:U.UI,browser:!0,args:[{name:"text",type:"string",required:!0,positional:!0,help:"The text content of the tweet"},{name:"images",type:"string",required:!1,help:"Image paths, comma-separated, max 4 (jpg/png/gif/webp)"}],columns:["status","message","text","id","url"],func:async(z,Q)=>{if(!z)throw new G("Browser session required for twitter post");const H=Q.images?h(String(Q.images)):[],Y=String(Q.text??"");try{await z.goto(S,{waitUntil:"load",settleMs:2500});await z.wait({selector:'[data-testid="tweetTextarea_0"]',timeout:15})}catch(W){if(H.length>0)throw W;const K=await i(z,Y);return[{status:K?.ok?"success":"failed",message:K?.message??"Tweet failed to post.",text:Y,...K?.id?{id:K.id}:{},...K?.url?{url:K.url}:{}}]}if(H.length>0){await z.wait({selector:J,timeout:20});if(z.setFileInput)try{await z.setFileInput(H,J)}catch(K){if(!b(K))throw K;await f(z,H)}else await f(z,H);const W=await E(z,H.length);if(!W?.ok)return[{status:"failed",message:W?.message??`Image upload timed out (${F/1000}s).`,text:Y}]}const $=await n(z,Y);if(!$?.ok)return[{status:"failed",message:$?.message??"Could not type tweet text.",text:Y}];await z.wait(1);const Z=await u(z,Y);return[{status:Z?.ok?"success":"failed",message:Z?.message??"Tweet failed to post.",text:Y,...Z?.id?{id:Z.id}:{},...Z?.url?{url:Z.url}:{}}]}});
158
+ }`));if(V?.httpStatus===401||V?.httpStatus===403)throw new D("x.com",`Twitter CreateTweet returned HTTP ${V.httpStatus}`);if(!V?.ok){const q=C(V?.bodyJson);return{ok:!1,message:m("CreateTweet",V?.httpStatus??0,q||void 0)}}const v=i(V.bodyJson);if(!v){const q=C(V.bodyJson);return{ok:!1,message:q?`CreateTweet failed: ${q}`:"CreateTweet returned no created tweet id."}}return{ok:!0,message:"Tweet posted successfully.",...v}}w({site:"twitter",name:"post",access:"write",description:"Post a new tweet/thread",domain:"x.com",strategy:U.UI,browser:!0,args:[{name:"text",type:"string",required:!0,positional:!0,help:"The text content of the tweet"},{name:"images",type:"string",required:!1,help:"Image paths, comma-separated, max 4 (jpg/png/gif/webp)"}],columns:["status","message","text","id","url"],func:async(z,Q)=>{if(!z)throw new G("Browser session required for twitter post");const H=Q.images?_(String(Q.images)):[],Y=String(Q.text??"");S(Y,{label:"推文内容"});try{await z.goto(x,{waitUntil:"load",settleMs:2500});await z.wait({selector:'[data-testid="tweetTextarea_0"]',timeout:15})}catch(W){if(H.length>0)throw W;const K=await g(z,Y);return[{status:K?.ok?"success":"failed",message:K?.message??"Tweet failed to post.",text:Y,...K?.id?{id:K.id}:{},...K?.url?{url:K.url}:{}}]}if(H.length>0){await z.wait({selector:J,timeout:20});if(z.setFileInput)try{await z.setFileInput(H,J)}catch(K){if(!b(K))throw K;await f(z,H)}else await f(z,H);const W=await u(z,H.length);if(!W?.ok)return[{status:"failed",message:W?.message??`Image upload timed out (${j/1000}s).`,text:Y}]}const $=await n(z,Y);if(!$?.ok)return[{status:"failed",message:$?.message??"Could not type tweet text.",text:Y}];await z.wait(1);const Z=await l(z,Y);return[{status:Z?.ok?"success":"failed",message:Z?.message??"Tweet failed to post.",text:Y,...Z?.id?{id:Z.id}:{},...Z?.url?{url:Z.url}:{}}]}});
@@ -1,4 +1,4 @@
1
- import{CommandExecutionError as N}from"@jackwener/opencli/errors";import{cli as O,Strategy as P}from"@jackwener/opencli/registry";O({site:"twitter",name:"reply-dm",access:"write",description:"Send a message to recent DM conversations",domain:"x.com",strategy:P.UI,browser:!0,args:[{name:"text",type:"string",required:!0,positional:!0,help:'Message text to send (e.g. "我的微信 wxkabi")'},{name:"max",type:"int",required:!1,default:20,help:"Maximum number of conversations to reply to (default: 20)"},{name:"skip-replied",type:"boolean",required:!1,default:!0,help:"Skip conversations where you already sent the same text (default: true)"},{name:"timeout",type:"int",required:!1,default:600,help:"Max seconds for the overall command (default: 600 — batch op)"}],columns:["index","status","user","message"],func:async(b,A)=>{if(!b)throw new N("Browser session required for twitter reply-dm");const H=A.text,F=A.max??20,J=A["skip-replied"]!==!1,f=[];let B=0;await b.goto("https://x.com/messages");await b.wait({selector:'[data-testid="primaryColumn"]'});const K=F+10,D=await b.evaluate(`(async () => {
1
+ import{CommandExecutionError as N}from"@jackwener/opencli/errors";import{cli as O,Strategy as P}from"@jackwener/opencli/registry";import{assertLiteralContent as Q}from"../_shared/content-guard.js";O({site:"twitter",name:"reply-dm",access:"write",description:"Send a message to recent DM conversations",domain:"x.com",strategy:P.UI,browser:!0,args:[{name:"text",type:"string",required:!0,positional:!0,help:'Message text to send (e.g. "我的微信 wxkabi")'},{name:"max",type:"int",required:!1,default:20,help:"Maximum number of conversations to reply to (default: 20)"},{name:"skip-replied",type:"boolean",required:!1,default:!0,help:"Skip conversations where you already sent the same text (default: true)"},{name:"timeout",type:"int",required:!1,default:600,help:"Max seconds for the overall command (default: 600 — batch op)"}],columns:["index","status","user","message"],func:async(b,A)=>{if(!b)throw new N("Browser session required for twitter reply-dm");const F=A.text;Q(F,{label:"私信内容"});const G=A.max??20,J=A["skip-replied"]!==!1,f=[];let B=0;await b.goto("https://x.com/messages");await b.wait({selector:'[data-testid="primaryColumn"]'});const K=G+10,D=await b.evaluate(`(async () => {
2
2
  try {
3
3
  // Wait for initial items
4
4
  let attempts = 0;
@@ -63,9 +63,9 @@ import{CommandExecutionError as N}from"@jackwener/opencli/errors";import{cli as
63
63
  } catch(e) {
64
64
  return { ok: false, error: String(e), conversations: [], total: 0 };
65
65
  }
66
- })()`);if(!D?.ok||!D.conversations?.length)return[{index:1,status:"info",user:"System",message:"No conversations found"}];const M=D.conversations;for(const q of M){if(B>=F)break;const G=q.convId?`https://x.com/messages/${q.convId}`:q.href;if(!G)continue;await b.goto(G);await b.wait(3);const z=await b.evaluate(`(async () => {
66
+ })()`);if(!D?.ok||!D.conversations?.length)return[{index:1,status:"info",user:"System",message:"No conversations found"}];const M=D.conversations;for(const q of M){if(B>=G)break;const H=q.convId?`https://x.com/messages/${q.convId}`:q.href;if(!H)continue;await b.goto(H);await b.wait(3);const z=await b.evaluate(`(async () => {
67
67
  try {
68
- const messageText = ${JSON.stringify(H)};
68
+ const messageText = ${JSON.stringify(F)};
69
69
  const skipReplied = ${J};
70
70
 
71
71
  // Get username from conversation
@@ -1,11 +1,11 @@
1
- import*as $ from"node:fs";import*as C from"node:path";import{CommandExecutionError as W}from"@jackwener/opencli/errors";import{cli as N,Strategy as f}from"@jackwener/opencli/registry";import{parseTweetUrl as F}from"./shared.js";import{COMPOSER_FILE_INPUT_SELECTOR as D,attachComposerImage as j,downloadRemoteImage as v,resolveImagePath as b}from"./utils.js";const X='[data-testid="tweetTextarea_0"]',K=500,Y=15000;function Z(q){return`https://x.com/compose/post?in_reply_to=${F(q).id}`}function Q(q){return(q instanceof Error?q.message:String(q)).includes("Promise was collected")}async function B(q,z){await q.goto(Z(z),{waitUntil:"load",settleMs:2500});try{await q.wait({selector:X,timeout:15});return{ok:!0}}catch{await q.goto(z,{waitUntil:"load",settleMs:2500});const A=await q.evaluate(`(() => {
1
+ import*as $ from"node:fs";import*as C from"node:path";import{CommandExecutionError as W}from"@jackwener/opencli/errors";import{cli as N,Strategy as f}from"@jackwener/opencli/registry";import{parseTweetUrl as F}from"./shared.js";import{assertLiteralContent as D}from"../_shared/content-guard.js";import{COMPOSER_FILE_INPUT_SELECTOR as v,attachComposerImage as b,downloadRemoteImage as j,resolveImagePath as B}from"./utils.js";const X='[data-testid="tweetTextarea_0"]',K=500,Y=15000;function Z(q){return`https://x.com/compose/post?in_reply_to=${F(q).id}`}function Q(q){return(q instanceof Error?q.message:String(q)).includes("Promise was collected")}async function h(q,z){await q.goto(Z(z),{waitUntil:"load",settleMs:2500});try{await q.wait({selector:X,timeout:15});return{ok:!0}}catch{await q.goto(z,{waitUntil:"load",settleMs:2500});const A=await q.evaluate(`(() => {
2
2
  const visible = (el) => !!el && (el.offsetParent !== null || el.getClientRects().length > 0);
3
3
  const buttons = Array.from(document.querySelectorAll('[data-testid="reply"]'));
4
4
  const btn = buttons.find((el) => visible(el) && !el.disabled && el.getAttribute('aria-disabled') !== 'true');
5
5
  if (!btn) return { ok: false, message: 'Could not find the reply button on the target tweet.' };
6
6
  btn.click();
7
7
  return { ok: true };
8
- })()`);if(!A?.ok)return A;await q.wait({selector:X,timeout:15});return{ok:!0}}}async function h(q,z){return q.evaluate(`(async () => {
8
+ })()`);if(!A?.ok)return A;await q.wait({selector:X,timeout:15});return{ok:!0}}}async function x(q,z){return q.evaluate(`(async () => {
9
9
  try {
10
10
  const visible = (el) => !!el && (el.offsetParent !== null || el.getClientRects().length > 0);
11
11
  const boxes = Array.from(document.querySelectorAll('[data-testid="tweetTextarea_0"]'));
@@ -38,7 +38,7 @@ import*as $ from"node:fs";import*as C from"node:path";import{CommandExecutionErr
38
38
  } catch (e) {
39
39
  return { ok: false, message: e.toString() };
40
40
  }
41
- })()`)}async function x(q){const z=Math.ceil(Y/K);return q.evaluate(`(async () => {
41
+ })()`)}async function y(q){const z=Math.ceil(Y/K);return q.evaluate(`(async () => {
42
42
  try {
43
43
  const visible = (el) => !!el && (el.offsetParent !== null || el.getClientRects().length > 0);
44
44
  for (let i = 0; i < ${JSON.stringify(z)}; i++) {
@@ -68,7 +68,7 @@ import*as $ from"node:fs";import*as C from"node:path";import{CommandExecutionErr
68
68
  message: 'Reply posted successfully.',
69
69
  url: link?.href || link?.getAttribute('href') || undefined
70
70
  };
71
- })()`)}async function y(q,z){const A=Math.ceil(Y/K);try{return await q.evaluate(`(async () => {
71
+ })()`)}async function u(q,z){const A=Math.ceil(Y/K);try{return await q.evaluate(`(async () => {
72
72
  const expected = ${JSON.stringify(z)};
73
73
  const normalize = s => String(s || '').replace(/\\u00a0/g, ' ').replace(/\\s+/g, ' ').trim();
74
74
  const expectedText = normalize(expected);
@@ -94,4 +94,4 @@ import*as $ from"node:fs";import*as C from"node:path";import{CommandExecutionErr
94
94
  if (!composerStillHasText) return { ok: true, message: 'Reply posted successfully.' };
95
95
  }
96
96
  return { ok: false, message: 'Reply submission did not complete before timeout.' };
97
- })()`)}catch(G){if(!Q(G))throw G;await q.wait(2);const H=await L(q);if(H?.ok)return H;throw G}}async function u(q,z){const A=await h(q,z);if(!A?.ok)return A;let G;try{G=await x(q)}catch(H){if(!Q(H))throw H}if(G&&!G.ok)return G;return y(q,z)}N({site:"twitter",name:"reply",access:"write",description:"Reply to a specific tweet, optionally with a local or remote image",domain:"x.com",strategy:f.UI,browser:!0,args:[{name:"url",type:"string",required:!0,positional:!0,help:"The URL of the tweet to reply to"},{name:"text",type:"string",required:!0,positional:!0,help:"The text content of your reply"},{name:"image",help:"Optional local image path to attach to the reply"},{name:"image-url",help:"Optional remote image URL to download and attach to the reply"}],columns:["status","message","text","url"],func:async(q,z)=>{if(!q)throw new W("Browser session required for twitter reply");if(z.image&&z["image-url"])throw new W("Use either --image or --image-url, not both.");let A,G;try{if(z.image)A=b(z.image);else if(z["image-url"]){const V=await v(z["image-url"]);A=V.absPath;G=V.cleanupDir}const H=await B(q,z.url);if(!H?.ok)return[{status:"failed",message:H?.message??"Could not open the reply composer.",text:z.text}];if(A){await q.wait({selector:D,timeout:20});await j(q,A)}const J=await u(q,z.text);return[{status:J.ok?"success":"failed",message:J.message,text:z.text,...J.url?{url:J.url}:{},...z.image?{image:z.image}:{},...z["image-url"]?{"image-url":z["image-url"]}:{}}]}finally{if(G)$.rmSync(G,{recursive:!0,force:!0})}}});export const __test__={buildReplyComposerUrl:Z,isPromiseCollectedError:Q};
97
+ })()`)}catch(G){if(!Q(G))throw G;await q.wait(2);const H=await L(q);if(H?.ok)return H;throw G}}async function O(q,z){const A=await x(q,z);if(!A?.ok)return A;let G;try{G=await y(q)}catch(H){if(!Q(H))throw H}if(G&&!G.ok)return G;return u(q,z)}N({site:"twitter",name:"reply",access:"write",description:"Reply to a specific tweet, optionally with a local or remote image",domain:"x.com",strategy:f.UI,browser:!0,args:[{name:"url",type:"string",required:!0,positional:!0,help:"The URL of the tweet to reply to"},{name:"text",type:"string",required:!0,positional:!0,help:"The text content of your reply"},{name:"image",help:"Optional local image path to attach to the reply"},{name:"image-url",help:"Optional remote image URL to download and attach to the reply"}],columns:["status","message","text","url"],func:async(q,z)=>{if(!q)throw new W("Browser session required for twitter reply");D(z.text,{label:"回复内容"});if(z.image&&z["image-url"])throw new W("Use either --image or --image-url, not both.");let A,G;try{if(z.image)A=B(z.image);else if(z["image-url"]){const V=await j(z["image-url"]);A=V.absPath;G=V.cleanupDir}const H=await h(q,z.url);if(!H?.ok)return[{status:"failed",message:H?.message??"Could not open the reply composer.",text:z.text}];if(A){await q.wait({selector:v,timeout:20});await b(q,A)}const J=await O(q,z.text);return[{status:J.ok?"success":"failed",message:J.message,text:z.text,...J.url?{url:J.url}:{},...z.image?{image:z.image}:{},...z["image-url"]?{"image-url":z["image-url"]}:{}}]}finally{if(G)$.rmSync(G,{recursive:!0,force:!0})}}});export const __test__={buildReplyComposerUrl:Z,isPromiseCollectedError:Q};
@@ -1,11 +1,11 @@
1
- import*as A from"node:fs";import*as U from"node:path";import{CommandExecutionError as H,ArgumentError as W}from"@jackwener/opencli/errors";import{cli as Jz,Strategy as Kz}from"@jackwener/opencli/registry";const Qz="https://creator.xiaohongshu.com/publish/publish?from=menu_left&target=image",S=9,C=20,T=3000,x="|||",B="基础",n=[["基础","默认兜底,万能"],["边框","金句/要点卡"],["备忘","提醒/随手记"],["清新","日常/清单贴士"],["涂写","随笔/碎碎念"],["便签","笔记/提醒"],["光影","情绪/文艺"],["涂鸦","趣味/童话"],["简约","干货/观点"],["手写","日记/情感"],["插图","生活方式/轻松话题"],["美漫","活力/趣味/故事感"],["弥散","弥散光氛围"],["柔和","柔和/温柔金句"],["印刷","印刷海报/排版"],["科技","科技/产品"],["贺卡","节日祝福"],["札记","艺术/水彩氛围"],["书摘","书摘/引用"],["手帐","手帐拼贴"],["几何","醒目/有力主张"]],Lz=n.map(([z])=>z),P="文字配图",u="再写一张",d="生成图片",c="下一步",y=".tiptap.ProseMirror",Zz=["_onPublish","onPublish","_onSubmit","_handlePublish"],$z=["_onSave","_onSaveDraft","_onDraft"],w=['[contenteditable="true"][placeholder*="标题"]','[contenteditable="true"][placeholder*="赞"]','input[placeholder*="标题"]','input[placeholder*="title" i]','[contenteditable="true"][class*="title"]','input[maxlength="20"]','input[class*="title"]',".title-input input",".note-title input","input[maxlength]"],E=['[contenteditable="true"][class*="content"]','[contenteditable="true"][class*="editor"]','[contenteditable="true"][placeholder*="描述"]','[contenteditable="true"][placeholder*="正文"]','[contenteditable="true"][placeholder*="内容"]','.note-content [contenteditable="true"]','.editor-content [contenteditable="true"]','[contenteditable="true"]:not([placeholder*="标题"]):not([placeholder*="赞"]):not([placeholder*="title" i])'],m={".jpg":"image/jpeg",".jpeg":"image/jpeg",".png":"image/png",".gif":"image/gif",".webp":"image/webp"};function X(z){if(z&&typeof z==="object"&&typeof z.session==="string"&&Object.prototype.hasOwnProperty.call(z,"data"))return z.data;return z}function jz(z){return z.map((J)=>{const K=U.resolve(J);if(!A.existsSync(K))throw new W(`Image file not found: ${K}`);const Z=U.extname(K).toLowerCase();if(!m[Z])throw new W(`Unsupported image format "${Z}". Supported: jpg, png, gif, webp`);return K})}const i='input[type="file"][accept*="image"],input[type="file"][accept*=".jpg"],input[type="file"][accept*=".jpeg"],input[type="file"][accept*=".png"],input[type="file"][accept*=".gif"],input[type="file"][accept*=".webp"]';async function qz(z,J=15000){const K=500,Z=Math.max(1,Math.ceil(J/K));for(let Q=0;Q<Z;Q++){if(await z.evaluate(`
1
+ import*as y from"node:fs";import*as U from"node:path";import{CommandExecutionError as H,ArgumentError as Y}from"@jackwener/opencli/errors";import{cli as Kz,Strategy as Qz}from"@jackwener/opencli/registry";import{assertLiteralContent as Zz}from"../_shared/content-guard.js";const $z="https://creator.xiaohongshu.com/publish/publish?from=menu_left&target=image",C=9,T=20,x=3000,d="|||",v="基础",u=[["基础","默认兜底,万能"],["边框","金句/要点卡"],["备忘","提醒/随手记"],["清新","日常/清单贴士"],["涂写","随笔/碎碎念"],["便签","笔记/提醒"],["光影","情绪/文艺"],["涂鸦","趣味/童话"],["简约","干货/观点"],["手写","日记/情感"],["插图","生活方式/轻松话题"],["美漫","活力/趣味/故事感"],["弥散","弥散光氛围"],["柔和","柔和/温柔金句"],["印刷","印刷海报/排版"],["科技","科技/产品"],["贺卡","节日祝福"],["札记","艺术/水彩氛围"],["书摘","书摘/引用"],["手帐","手帐拼贴"],["几何","醒目/有力主张"]],Rz=u.map(([z])=>z),M="文字配图",n="再写一张",E="生成图片",c="下一步",L=".tiptap.ProseMirror",qz=["_onPublish","onPublish","_onSubmit","_handlePublish"],jz=["_onSave","_onSaveDraft","_onDraft"],w=['[contenteditable="true"][placeholder*="标题"]','[contenteditable="true"][placeholder*="赞"]','input[placeholder*="标题"]','input[placeholder*="title" i]','[contenteditable="true"][class*="title"]','input[maxlength="20"]','input[class*="title"]',".title-input input",".note-title input","input[maxlength]"],m=['[contenteditable="true"][class*="content"]','[contenteditable="true"][class*="editor"]','[contenteditable="true"][placeholder*="描述"]','[contenteditable="true"][placeholder*="正文"]','[contenteditable="true"][placeholder*="内容"]','.note-content [contenteditable="true"]','.editor-content [contenteditable="true"]','[contenteditable="true"]:not([placeholder*="标题"]):not([placeholder*="赞"]):not([placeholder*="title" i])'],i={".jpg":"image/jpeg",".jpeg":"image/jpeg",".png":"image/png",".gif":"image/gif",".webp":"image/webp"};function X(z){if(z&&typeof z==="object"&&typeof z.session==="string"&&Object.prototype.hasOwnProperty.call(z,"data"))return z.data;return z}function Vz(z){return z.map((J)=>{const K=U.resolve(J);if(!y.existsSync(K))throw new Y(`Image file not found: ${K}`);const Z=U.extname(K).toLowerCase();if(!i[Z])throw new Y(`Unsupported image format "${Z}". Supported: jpg, png, gif, webp`);return K})}const o='input[type="file"][accept*="image"],input[type="file"][accept*=".jpg"],input[type="file"][accept*=".jpeg"],input[type="file"][accept*=".png"],input[type="file"][accept*=".gif"],input[type="file"][accept*=".webp"]';async function Hz(z,J=15000){const K=500,Z=Math.max(1,Math.ceil(J/K));for(let Q=0;Q<Z;Q++){if(await z.evaluate(`
2
2
  (() => {
3
- const sels = ${JSON.stringify(i)};
3
+ const sels = ${JSON.stringify(o)};
4
4
  return !!document.querySelector(sels);
5
5
  })()
6
- `))return!0;if(Q<Z-1)await z.wait({time:K/1000})}return!1}async function o(z,J){if(!await qz(z))return{ok:!1,count:0,error:"No file input found on page (waited 15s; publish surface did not finish rendering)"};if(z.setFileInput)try{await z.setFileInput(J,i);return{ok:!0,count:J.length}}catch($){const q=$ instanceof Error?$.message:String($);if(q.includes("Unknown action")||q.includes("not supported")||q.includes("Not allowed"));else return{ok:!1,count:0,error:q}}const Z=J.map(($)=>{const q=A.readFileSync($).toString("base64"),Y=U.extname($).toLowerCase();return{name:U.basename($),mimeType:m[Y],base64:q}}),Q=Z.reduce(($,q)=>$+q.base64.length,0);if(Q>500000)console.warn(`[warn] Total image payload is ${(Q/1024/1024).toFixed(1)}MB (base64). This may fail with the browser bridge. Update the extension to v1.6+ for CDP-based upload, or compress images before publishing.`);const j=JSON.stringify(Z);return z.evaluate(`
6
+ `))return!0;if(Q<Z-1)await z.wait({time:K/1000})}return!1}async function l(z,J){if(!await Hz(z))return{ok:!1,count:0,error:"No file input found on page (waited 15s; publish surface did not finish rendering)"};if(z.setFileInput)try{await z.setFileInput(J,o);return{ok:!0,count:J.length}}catch($){const j=$ instanceof Error?$.message:String($);if(j.includes("Unknown action")||j.includes("not supported")||j.includes("Not allowed"));else return{ok:!1,count:0,error:j}}const Z=J.map(($)=>{const j=y.readFileSync($).toString("base64"),W=U.extname($).toLowerCase();return{name:U.basename($),mimeType:i[W],base64:j}}),Q=Z.reduce(($,j)=>$+j.base64.length,0);if(Q>500000)console.warn(`[warn] Total image payload is ${(Q/1024/1024).toFixed(1)}MB (base64). This may fail with the browser bridge. Update the extension to v1.6+ for CDP-based upload, or compress images before publishing.`);const q=JSON.stringify(Z);return z.evaluate(`
7
7
  (async () => {
8
- const images = ${j};
8
+ const images = ${q};
9
9
 
10
10
  const inputs = Array.from(document.querySelectorAll('input[type="file"]'));
11
11
  const input = inputs.find(el => {
@@ -41,11 +41,11 @@ import*as A from"node:fs";import*as U from"node:path";import{CommandExecutionErr
41
41
 
42
42
  return { ok: true, count: dt.files.length };
43
43
  })()
44
- `)}async function l(z,J=30000){const K=2000,Z=Math.ceil(J/K);for(let Q=0;Q<Z;Q++){if(!await z.evaluate(`
44
+ `)}async function r(z,J=30000){const K=2000,Z=Math.ceil(J/K);for(let Q=0;Q<Z;Q++){if(!await z.evaluate(`
45
45
  () => !!document.querySelector(
46
46
  '[class*="upload"][class*="progress"], [class*="uploading"], [class*="loading"][class*="image"]'
47
47
  )
48
- `))return;await z.wait({time:K/1000})}}async function r(z,J,K,Z){const Q=await z.evaluate(`
48
+ `))return;await z.wait({time:K/1000})}}async function g(z,J,K,Z){const Q=await z.evaluate(`
49
49
  (function(selectors) {
50
50
  const __opencli_xhs_fill_phase = "locate";
51
51
  for (const sel of selectors) {
@@ -60,7 +60,7 @@ import*as A from"node:fs";import*as U from"node:path";import{CommandExecutionErr
60
60
  }
61
61
  return { ok: false };
62
62
  })(${JSON.stringify(J)})
63
- `);if(!Q.ok){await z.screenshot({path:`/tmp/xhs_publish_${Z}_debug.png`});throw Error(`Could not find ${Z} input. Debug screenshot: /tmp/xhs_publish_${Z}_debug.png`)}const j=()=>z.evaluate(`
63
+ `);if(!Q.ok){await z.screenshot({path:`/tmp/xhs_publish_${Z}_debug.png`});throw Error(`Could not find ${Z} input. Debug screenshot: /tmp/xhs_publish_${Z}_debug.png`)}const q=()=>z.evaluate(`
64
64
  ((selector, expectedText) => {
65
65
  const __opencli_xhs_fill_phase = "apply";
66
66
  const normalize = (value) => (value || '').replace(/\\s+/g, ' ').trim();
@@ -167,7 +167,7 @@ import*as A from"node:fs";import*as U from"node:path";import{CommandExecutionErr
167
167
  const actual = normalize(el.innerText || el.textContent || '');
168
168
  return { ok: actual === normalize(expectedText), actual };
169
169
  })(${JSON.stringify(Q.sel)}, ${JSON.stringify(K)})
170
- `)}catch{$=await j()}}else $=await j();if(!$?.ok){await z.screenshot({path:`/tmp/xhs_publish_${Z}_debug.png`});const q=typeof $?.actual==="string"?$.actual:"";throw Error(`Failed to set ${Z}. Expected "${K}", got "${q}". Debug screenshot: /tmp/xhs_publish_${Z}_debug.png`)}}async function Vz(z,J){return X(await z.evaluate(`
170
+ `)}catch{$=await q()}}else $=await q();if(!$?.ok){await z.screenshot({path:`/tmp/xhs_publish_${Z}_debug.png`});const j=typeof $?.actual==="string"?$.actual:"";throw Error(`Failed to set ${Z}. Expected "${K}", got "${j}". Debug screenshot: /tmp/xhs_publish_${Z}_debug.png`)}}async function Gz(z,J){return X(await z.evaluate(`
171
171
  (selectors => {
172
172
  const el = selectors
173
173
  .map(sel => Array.from(document.querySelectorAll(sel)))
@@ -183,7 +183,7 @@ import*as A from"node:fs";import*as U from"node:path";import{CommandExecutionErr
183
183
  selection?.addRange(range);
184
184
  return true;
185
185
  })(${JSON.stringify(J)})
186
- `))}function Mz(z,{click:J=!1}={}){return`
186
+ `))}function _z(z,{click:J=!1}={}){return`
187
187
  (topicName => {
188
188
  const norm = (value) => (value || '').replace(/^#/, '').replace(/\\s+/g, '').trim();
189
189
  const want = norm(topicName);
@@ -223,7 +223,7 @@ import*as A from"node:fs";import*as U from"node:path";import{CommandExecutionErr
223
223
  text: (target.innerText || target.textContent || '').trim().slice(0, 40),
224
224
  };
225
225
  })(${JSON.stringify(z)})
226
- `}function bz(z,J){return`
226
+ `}function Sz(z,J){return`
227
227
  ((topicName, selectors) => {
228
228
  const norm = (value) => (value || '').replace(/^#/, '').replace(/\\s+/g, '').trim();
229
229
  const want = norm(topicName);
@@ -272,14 +272,14 @@ import*as A from"node:fs";import*as U from"node:path";import{CommandExecutionErr
272
272
  }
273
273
  return count;
274
274
  })(${JSON.stringify(z)}, ${JSON.stringify(J)})
275
- `}async function Rz(z,J){const K=`#${J}`;if(typeof z.insertText==="function")try{await z.insertText(K);return!0}catch{}return X(await z.evaluate(`
275
+ `}async function Az(z,J){const K=`#${J}`;if(typeof z.insertText==="function")try{await z.insertText(K);return!0}catch{}return X(await z.evaluate(`
276
276
  (text => {
277
277
  const ok = document.execCommand('insertText', false, text);
278
278
  const active = document.activeElement;
279
279
  if (active) active.dispatchEvent(new Event('input', { bubbles: true }));
280
280
  return ok;
281
281
  })(${JSON.stringify(K)})
282
- `))}async function Hz(z,J,K){const Z=[];for(const Q of K){if(!await Vz(z,J))throw new H(`Could not attach topic "${Q}": body editor not found`);const $=Number(X(await z.evaluate(s(Q,J))))||0;if(typeof z.pressKey==="function")try{await z.pressKey("Enter")}catch{}if(typeof z.insertText!=="function")throw new H(`Could not attach topic "${Q}": page.insertText is unavailable`);try{await z.insertText(`#${Q}`)}catch{throw new H(`Could not attach topic "${Q}": failed to type inline topic query`)}await z.wait({time:1.2});if(typeof z.pressKey!=="function")throw new H(`Could not attach topic "${Q}": page.pressKey is unavailable`);try{await z.pressKey("Enter")}catch(Y){throw new H(`Could not attach topic "${Q}": failed to accept suggestion (${Y&&Y.message||Y})`)}await z.wait({time:0.8});if((Number(X(await z.evaluate(s(Q,J))))||0)<=$)throw new H(`Could not attach topic "${Q}": no real topic entity appeared after selection`);Z.push(Q);await z.wait({time:0.4})}return Z}async function Gz(z){const J=await z.evaluate(`
282
+ `))}async function Yz(z,J,K){const Z=[];for(const Q of K){if(!await Gz(z,J))throw new H(`Could not attach topic "${Q}": body editor not found`);const $=Number(X(await z.evaluate(s(Q,J))))||0;if(typeof z.pressKey==="function")try{await z.pressKey("Enter")}catch{}if(typeof z.insertText!=="function")throw new H(`Could not attach topic "${Q}": page.insertText is unavailable`);try{await z.insertText(`#${Q}`)}catch{throw new H(`Could not attach topic "${Q}": failed to type inline topic query`)}await z.wait({time:1.2});if(typeof z.pressKey!=="function")throw new H(`Could not attach topic "${Q}": page.pressKey is unavailable`);try{await z.pressKey("Enter")}catch(W){throw new H(`Could not attach topic "${Q}": failed to accept suggestion (${W&&W.message||W})`)}await z.wait({time:0.8});if((Number(X(await z.evaluate(s(Q,J))))||0)<=$)throw new H(`Could not attach topic "${Q}": no real topic entity appeared after selection`);Z.push(Q);await z.wait({time:0.4})}return Z}async function Xz(z){const J=await z.evaluate(`
283
283
  () => {
284
284
  const isVisible = (el) => {
285
285
  if (!el || el.offsetParent === null) return false;
@@ -328,7 +328,7 @@ import*as A from"node:fs";import*as U from"node:path";import{CommandExecutionErr
328
328
  }
329
329
  return { ok: false, visibleTexts };
330
330
  }
331
- `);if(J?.ok)await z.wait({time:1});return J}async function v(z,J,K=7000){const Z=500,Q=Math.max(1,Math.ceil(K/Z));let j={ok:!1};for(let $=0;$<Q;$++){j=X(await z.evaluate(`
331
+ `);if(J?.ok)await z.wait({time:1});return J}async function D(z,J,K=7000){const Z=500,Q=Math.max(1,Math.ceil(K/Z));let q={ok:!1};for(let $=0;$<Q;$++){q=X(await z.evaluate(`
332
332
  ((cfg) => {
333
333
  const __opencli_xhs_click_label = { wantLabel: ${JSON.stringify(J)} };
334
334
  const wantLabel = ${JSON.stringify(J)};
@@ -363,10 +363,10 @@ import*as A from"node:fs";import*as U from"node:path";import{CommandExecutionErr
363
363
  }
364
364
  return { ok: false };
365
365
  })()
366
- `));if(j?.ok)return j;await z.wait({time:Z/1000})}return j}async function Yz(z){const J=await z.evaluate(`
366
+ `));if(q?.ok)return q;await z.wait({time:Z/1000})}return q}async function Wz(z){const J=await z.evaluate(`
367
367
  (() => {
368
368
  const __opencli_xhs_focus_card = true;
369
- const sel = ${JSON.stringify(y)};
369
+ const sel = ${JSON.stringify(L)};
370
370
  const editors = Array.from(document.querySelectorAll(sel)).filter((el) => el.offsetParent !== null);
371
371
  // Prefer the editor inside the active swiper slide if present.
372
372
  const active = editors.find((el) => el.closest('.swiper-slide-active')) || editors[editors.length - 1];
@@ -380,34 +380,34 @@ import*as A from"node:fs";import*as U from"node:path";import{CommandExecutionErr
380
380
  selection?.addRange(range);
381
381
  return { ok: true };
382
382
  })()
383
- `);return X(J)}async function Xz(z){const J=await z.evaluate(`
383
+ `);return X(J)}async function Fz(z){const J=await z.evaluate(`
384
384
  (() => {
385
385
  const __opencli_xhs_card_text = true;
386
- const sel = ${JSON.stringify(y)};
386
+ const sel = ${JSON.stringify(L)};
387
387
  const editors = Array.from(document.querySelectorAll(sel)).filter((el) => el.offsetParent !== null);
388
388
  const active = editors.find((el) => el.closest('.swiper-slide-active')) || editors[editors.length - 1];
389
389
  const text = active ? (active.innerText || active.textContent || '').trim() : '';
390
390
  return { ok: !!text, text };
391
391
  })()
392
- `);return X(J)}async function g(z){const J=await z.evaluate(`
392
+ `);return X(J)}async function p(z){const J=await z.evaluate(`
393
393
  (() => {
394
394
  const __opencli_xhs_card_count = true;
395
- const sel = ${JSON.stringify(y)};
395
+ const sel = ${JSON.stringify(L)};
396
396
  const editors = Array.from(document.querySelectorAll(sel)).filter((el) => el.offsetParent !== null);
397
397
  const active = editors.find((el) => el.closest('.swiper-slide-active')) || editors[editors.length - 1];
398
398
  const activeText = active ? (active.innerText || active.textContent || '').trim() : '';
399
399
  return { ok: true, count: editors.length, activeEmpty: !activeText };
400
400
  })()
401
- `);return X(J)}async function Wz(z,J=8000){const K=300,Z=Math.ceil(J/K);for(let Q=0;Q<Z;Q++){if((await g(z))?.count>=1)return!0;await z.wait({time:K/1000})}return!1}async function Fz(z,J,K=6000){const Z=300,Q=Math.ceil(K/Z);for(let j=0;j<Q;j++){const $=await g(z);if($?.count>=J&&$?.activeEmpty)return!0;await z.wait({time:Z/1000})}return!1}async function Oz(z,J,K=4){for(let Z=0;Z<K;Z++){if(!(await v(z,u))?.ok)return!1;if(await Fz(z,J,2500))return!0}return!1}async function Uz(z){const J=await z.evaluate(`
401
+ `);return X(J)}async function Oz(z,J=8000){const K=300,Z=Math.ceil(J/K);for(let Q=0;Q<Z;Q++){if((await p(z))?.count>=1)return!0;await z.wait({time:K/1000})}return!1}async function Uz(z,J,K=6000){const Z=300,Q=Math.ceil(K/Z);for(let q=0;q<Q;q++){const $=await p(z);if($?.count>=J&&$?.activeEmpty)return!0;await z.wait({time:Z/1000})}return!1}async function Bz(z,J,K=4){for(let Z=0;Z<K;Z++){if(!(await D(z,n))?.ok)return!1;if(await Uz(z,J,2500))return!0}return!1}async function vz(z){const J=await z.evaluate(`
402
402
  (() => {
403
403
  const __opencli_xhs_preview_ready = true;
404
404
  const ready = Array.from(document.querySelectorAll('button'))
405
405
  .some((b) => b.offsetParent !== null && (b.innerText || '').replace(/\\s+/g, '') === '下一步');
406
406
  return { ok: ready };
407
407
  })()
408
- `);return X(J)}async function Bz(z,J=6){const K=400,Z=Math.ceil(3000/K);for(let Q=0;Q<J;Q++){if(!(await v(z,d))?.ok&&Q===0)return!1;for(let $=0;$<Z;$++){if((await Uz(z))?.ok)return!0;await z.wait({time:K/1000})}}return!1}async function vz(z,J,K){J=String(J).replace(/\\n/g,`
409
- `);if(!(await Yz(z))?.ok)throw new H(`文字配图: could not focus card editor #${K+1}`);if(typeof z.insertText==="function"){const j=J.split(`
410
- `);for(let $=0;$<j.length;$++){if($>0&&typeof z.pressKey==="function")await z.pressKey("Enter");if(j[$])await z.insertText(j[$])}}else await z.evaluate(`(t => document.execCommand('insertText', false, t))(${JSON.stringify(J)})`);await z.wait({time:0.4});if(!(await Xz(z))?.ok)throw new H(`文字配图: card editor #${K+1} is empty after typing`)}async function hz(z,J){if(!J||J===B)return B;const K=X(await z.evaluate(`
408
+ `);return X(J)}async function Dz(z,J=6){const K=400,Z=Math.ceil(3000/K);for(let Q=0;Q<J;Q++){if(!(await D(z,E))?.ok&&Q===0)return!1;for(let $=0;$<Z;$++){if((await vz(z))?.ok)return!0;await z.wait({time:K/1000})}}return!1}async function Iz(z,J,K){J=String(J).replace(/\\n/g,`
409
+ `);if(!(await Wz(z))?.ok)throw new H(`文字配图: could not focus card editor #${K+1}`);if(typeof z.insertText==="function"){const q=J.split(`
410
+ `);for(let $=0;$<q.length;$++){if($>0&&typeof z.pressKey==="function")await z.pressKey("Enter");if(q[$])await z.insertText(q[$])}}else await z.evaluate(`(t => document.execCommand('insertText', false, t))(${JSON.stringify(J)})`);await z.wait({time:0.4});if(!(await Fz(z))?.ok)throw new H(`文字配图: card editor #${K+1} is empty after typing`)}async function hz(z,J){if(!J||J===v)return v;const K=X(await z.evaluate(`
411
411
  (async () => {
412
412
  const __opencli_xhs_card_styles = true;
413
413
  const want = ${JSON.stringify(J)};
@@ -436,7 +436,7 @@ import*as A from"node:fs";import*as U from"node:path";import{CommandExecutionErr
436
436
  }
437
437
  return { ok: seen.length > 0, styles: seen, found: false };
438
438
  })()
439
- `));if(!K?.found)throw new H(`文字配图: requested style "${J}" is not available for this content (options: ${(K?.styles||[]).join(" / ")||"none"}). Choose an available style or omit --card-style to use ${B}.`);if(!(await v(z,J))?.ok)throw new H(`文字配图: could not click requested style "${J}".`);await z.wait({time:0.6});return J}async function Dz(z){const J=await z.evaluate(`
439
+ `));if(!K?.found)throw new H(`文字配图: requested style "${J}" is not available for this content (options: ${(K?.styles||[]).join(" / ")||"none"}). Choose an available style or omit --card-style to use ${v}.`);if(!(await D(z,J))?.ok)throw new H(`文字配图: could not click requested style "${J}".`);await z.wait({time:0.6});return J}async function fz(z){const J=await z.evaluate(`
440
440
  (() => {
441
441
  const __opencli_xhs_composer_media_count = true;
442
442
  const visibleBox = (el) => {
@@ -468,7 +468,7 @@ import*as A from"node:fs";import*as U from"node:path";import{CommandExecutionErr
468
468
  }
469
469
  return { ok: true, count };
470
470
  })()
471
- `);return X(J)}async function p(z,J,K){const Z=await Dz(z);if(!Z||typeof Z.count!=="number")throw new H(`${K}: could not verify current composer media count`);if(Z.count<J){await z.screenshot({path:"/tmp/xhs_publish_media_debug.png"});throw new H(`${K}: expected at least ${J} visible media item(s), got ${Z.count}. Debug screenshot: /tmp/xhs_publish_media_debug.png`)}}async function fz(z,J,K){if(!(await v(z,P))?.ok){await z.screenshot({path:"/tmp/xhs_publish_textimage_debug.png"});throw new H(`文字配图: could not click "${P}" entry. Debug: /tmp/xhs_publish_textimage_debug.png`)}if(!await Wz(z)){await z.screenshot({path:"/tmp/xhs_publish_textimage_debug.png"});throw new H(`文字配图: 写文字 card editor did not appear after clicking "${P}". Debug: /tmp/xhs_publish_textimage_debug.png`)}for(let q=0;q<J.length;q++){if(q>0){if(!await Oz(z,q+1)){await z.screenshot({path:"/tmp/xhs_publish_addcard_debug.png"});throw new H(`文字配图: new card editor #${q+1} did not render after "${u}". Debug: /tmp/xhs_publish_addcard_debug.png`)}}await vz(z,J[q],q)}if(!await Bz(z)){await z.screenshot({path:"/tmp/xhs_publish_generate_debug.png"});throw new H(`文字配图: "${d}" did not advance to the 预览图片 step. `+"Debug: /tmp/xhs_publish_generate_debug.png")}const j=await hz(z,K);if(!(await v(z,c))?.ok){await z.screenshot({path:"/tmp/xhs_publish_next_debug.png"});throw new H(`文字配图: could not click "${c}". Debug: /tmp/xhs_publish_next_debug.png`)}await z.wait({time:2});return j}async function t(z){return z.evaluate(`
471
+ `);return X(J)}async function t(z,J,K){const Z=await fz(z);if(!Z||typeof Z.count!=="number")throw new H(`${K}: could not verify current composer media count`);if(Z.count<J){await z.screenshot({path:"/tmp/xhs_publish_media_debug.png"});throw new H(`${K}: expected at least ${J} visible media item(s), got ${Z.count}. Debug screenshot: /tmp/xhs_publish_media_debug.png`)}}async function Nz(z,J,K){if(!(await D(z,M))?.ok){await z.screenshot({path:"/tmp/xhs_publish_textimage_debug.png"});throw new H(`文字配图: could not click "${M}" entry. Debug: /tmp/xhs_publish_textimage_debug.png`)}if(!await Oz(z)){await z.screenshot({path:"/tmp/xhs_publish_textimage_debug.png"});throw new H(`文字配图: 写文字 card editor did not appear after clicking "${M}". Debug: /tmp/xhs_publish_textimage_debug.png`)}for(let j=0;j<J.length;j++){if(j>0){if(!await Bz(z,j+1)){await z.screenshot({path:"/tmp/xhs_publish_addcard_debug.png"});throw new H(`文字配图: new card editor #${j+1} did not render after "${n}". Debug: /tmp/xhs_publish_addcard_debug.png`)}}await Iz(z,J[j],j)}if(!await Dz(z)){await z.screenshot({path:"/tmp/xhs_publish_generate_debug.png"});throw new H(`文字配图: "${E}" did not advance to the 预览图片 step. `+"Debug: /tmp/xhs_publish_generate_debug.png")}const q=await hz(z,K);if(!(await D(z,c))?.ok){await z.screenshot({path:"/tmp/xhs_publish_next_debug.png"});throw new H(`文字配图: could not click "${c}". Debug: /tmp/xhs_publish_next_debug.png`)}await z.wait({time:2});return q}async function a(z){return z.evaluate(`
472
472
  () => {
473
473
  const text = (document.body?.innerText || '').replace(/s+/g, ' ').trim();
474
474
  const hasTitleInput = !!Array.from(document.querySelectorAll('input, textarea')).find((el) => {
@@ -498,7 +498,7 @@ import*as A from"node:fs";import*as U from"node:path";import{CommandExecutionErr
498
498
  const state = hasTitleInput ? 'editor_ready' : hasImageInput || !hasVideoSurface ? 'image_surface' : 'video_surface';
499
499
  return { state, hasTitleInput, hasImageInput, hasVideoSurface };
500
500
  }
501
- `)}async function Iz(z,J=5000){const K=500,Z=Math.max(1,Math.ceil(J/K));let Q=await t(z);for(let j=0;j<Z;j++){if(Q.state!=="video_surface")return Q;if(j<Z-1){await z.wait({time:K/1000});Q=await t(z)}}return Q}async function kz(z,J=1e4){const K=1000,Z=Math.ceil(J/K);for(let Q=0;Q<Z;Q++){if(await z.evaluate(`
501
+ `)}async function kz(z,J=5000){const K=500,Z=Math.max(1,Math.ceil(J/K));let Q=await a(z);for(let q=0;q<Z;q++){if(Q.state!=="video_surface")return Q;if(q<Z-1){await z.wait({time:K/1000});Q=await a(z)}}return Q}async function yz(z,J=1e4){const K=1000,Z=Math.ceil(J/K);for(let Q=0;Q<Z;Q++){if(await z.evaluate(`
502
502
  (() => {
503
503
  const sels = ${JSON.stringify(w)};
504
504
  for (const sel of sels) {
@@ -506,7 +506,7 @@ import*as A from"node:fs";import*as U from"node:path";import{CommandExecutionErr
506
506
  if (el && el.offsetParent !== null) return true;
507
507
  }
508
508
  return false;
509
- })()`))return!0;if(Q<Z-1)await z.wait({time:K/1000})}return!1}Jz({site:"xiaohongshu",name:"publish",access:"write",description:"小红书发布图文笔记 (creator center UI automation)",domain:"creator.xiaohongshu.com",strategy:Kz.COOKIE,browser:!0,navigateBefore:!1,args:[{name:"content",required:!0,positional:!0,help:"笔记正文"},{name:"title",required:!0,help:"笔记标题 (最多20字)"},{name:"images",required:!1,help:"图片路径,逗号分隔,最多9张 (jpg/png/gif/webp)"},{name:"card-text",required:!1,help:`文字配图卡片文字,多张卡片用 ${x} 分隔,卡内换行用 \\n`},{name:"card-style",required:!1,help:`文字配图卡片样式,运行时按页面实际选项匹配;找不到会失败。省略时使用${B}。可选: ${n.map(([z,J])=>`${z}(${J})`).join(" ")}`},{name:"topics",required:!1,help:"话题标签,逗号分隔,不含 # 号"},{name:"draft",type:"bool",default:!1,help:"保存为草稿,不直接发布"}],columns:["status","detail"],func:async(z,J)=>{if(!z)throw Error("Browser page required");const K=String(J.title??"").trim(),Z=String(J.content??"").trim(),Q=J.images?String(J.images).split(",").map((V)=>V.trim()).filter(Boolean):[],j=J.topics?String(J.topics).split(",").map((V)=>V.trim()).filter(Boolean):[],$=Boolean(J.draft),q=J["card-text"]?String(J["card-text"]):"",Y=q?q.split(x).map((V)=>V.trim()).filter(Boolean):[],L=J["card-style"]?String(J["card-style"]).trim():"",O=Y.length>0;if(!K)throw new W("--title is required");if(K.length>C)throw new W(`Title is ${K.length} chars — must be ≤ ${C}`);if(!Z)throw new W("Positional argument <content> is required");if(!O&&Q.length===0)throw new W("Provide --card-text (text-image mode) or --images (upload mode); neither was given.");if(Q.length>S)throw new W(`Too many images: ${Q.length} (max ${S})`);if(O&&Q.some((V)=>U.extname(V).toLowerCase()===".gif"))throw new W("文字配图模式追加的图片不支持 .gif(编辑器图片入口只接受 jpg/jpeg/png/webp)");const F=jz(Q);await z.goto(Qz);let h="";for(let V=0;V<30;V++){await z.wait({time:0.5});h=await z.evaluate("() => location.href");if(h.includes("creator.xiaohongshu.com"))break}if(!h.includes("creator.xiaohongshu.com")){await z.screenshot({path:"/tmp/xhs_publish_redirect_debug.png"});throw Error(`Redirected away from creator center (landed on ${h}) — session may have expired. `+"Re-capture browser login via: opencli xiaohongshu creator-profile. Debug screenshot: /tmp/xhs_publish_redirect_debug.png")}const D=await Gz(z);if((await Iz(z,D?.ok?5000:2000)).state==="video_surface"){await z.screenshot({path:"/tmp/xhs_publish_tab_debug.png"});const V=D?.ok?`clicked "${D.text}"`:`visible candidates: ${(D?.visibleTexts||[]).join(" | ")||"none"}`;throw Error("Still on the video publish page after trying to select 图文. "+`Details: ${V}. Debug screenshot: /tmp/xhs_publish_tab_debug.png`)}let f=L;if(O)f=await fz(z,Y,L);else{const V=await o(z,F);if(!V.ok){await z.screenshot({path:"/tmp/xhs_publish_upload_debug.png"});throw new H(`Image injection failed: ${V.error??"unknown"}. Debug screenshot: /tmp/xhs_publish_upload_debug.png`)}await z.wait({time:T/1000});await l(z)}if(!await kz(z)){await z.screenshot({path:"/tmp/xhs_publish_form_debug.png"});throw new H("Editing form did not appear after image acquisition. The page layout may have changed. Debug screenshot: /tmp/xhs_publish_form_debug.png")}if(O)await p(z,Y.length,"文字配图 generated images");if(O&&F.length>0){const V=await o(z,F);if(!V.ok){await z.screenshot({path:"/tmp/xhs_publish_append_debug.png"});throw new H(`Appending images failed: ${V.error??"unknown"}. Debug screenshot: /tmp/xhs_publish_append_debug.png`)}await z.wait({time:T/1000});await l(z);await p(z,Y.length+F.length,"文字配图 appended images")}await r(z,w,K,"title");await z.wait({time:0.5});await r(z,E,Z,"content");await z.wait({time:0.5});let N=[];if(j.length)N=await Hz(z,E,j);const M=$?["暂存离开","存草稿"]:["发布","发布笔记"],G=await z.evaluate(`
509
+ })()`))return!0;if(Q<Z-1)await z.wait({time:K/1000})}return!1}Kz({site:"xiaohongshu",name:"publish",access:"write",description:"小红书发布图文笔记 (creator center UI automation)",domain:"creator.xiaohongshu.com",strategy:Qz.COOKIE,browser:!0,navigateBefore:!1,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:!1,help:"图片路径,逗号分隔,最多9张 (jpg/png/gif/webp)"},{name:"card-text",required:!1,help:`文字配图卡片文字,多张卡片用 ${d} 分隔,卡内换行用 \\n`},{name:"card-style",required:!1,help:`文字配图卡片样式,运行时按页面实际选项匹配;找不到会失败。省略时使用${v}。可选: ${u.map(([z,J])=>`${z}(${J})`).join(" ")}`},{name:"topics",required:!1,help:"话题标签,逗号分隔,不含 # 号"},{name:"draft",type:"bool",default:!1,help:"保存为草稿,不直接发布"}],columns:["status","detail"],func:async(z,J)=>{if(!z)throw Error("Browser page required");const K=String(J.title??"").trim(),Z=J.file?String(J.file).trim():"";let Q=String(J.content??"").trim();if(Z){if(Q)throw new Y("正文位置参数和 --file 只能二选一,不能同时给");const V=U.resolve(Z);if(!y.existsSync(V))throw new Y(`--file 指向的文件不存在: ${V}`);Q=y.readFileSync(V,"utf-8").trim();if(!Q)throw new Y(`--file 指向的文件内容为空: ${V}`)}const q=J.images?String(J.images).split(",").map((V)=>V.trim()).filter(Boolean):[],$=J.topics?String(J.topics).split(",").map((V)=>V.trim()).filter(Boolean):[],j=Boolean(J.draft),W=J["card-text"]?String(J["card-text"]):"",B=W?W.split(d).map((V)=>V.trim()).filter(Boolean):[],b=J["card-style"]?String(J["card-style"]).trim():"",O=B.length>0;if(!K)throw new Y("--title is required");if(K.length>T)throw new Y(`Title is ${K.length} chars — must be ≤ ${T}`);if(!Q)throw new Y("缺少笔记正文:给位置参数 <content>(字面文本),或用 --file <本机文件路径>");Zz(Q,{fileFlag:"--file"});if(!O&&q.length===0)throw new Y("Provide --card-text (text-image mode) or --images (upload mode); neither was given.");if(q.length>C)throw new Y(`Too many images: ${q.length} (max ${C})`);if(O&&q.some((V)=>U.extname(V).toLowerCase()===".gif"))throw new Y("文字配图模式追加的图片不支持 .gif(编辑器图片入口只接受 jpg/jpeg/png/webp)");const F=Vz(q);await z.goto($z);let I="";for(let V=0;V<30;V++){await z.wait({time:0.5});I=await z.evaluate("() => location.href");if(I.includes("creator.xiaohongshu.com"))break}if(!I.includes("creator.xiaohongshu.com")){await z.screenshot({path:"/tmp/xhs_publish_redirect_debug.png"});throw Error(`Redirected away from creator center (landed on ${I}) — session may have expired. `+"Re-capture browser login via: opencli xiaohongshu creator-profile. Debug screenshot: /tmp/xhs_publish_redirect_debug.png")}const h=await Xz(z);if((await kz(z,h?.ok?5000:2000)).state==="video_surface"){await z.screenshot({path:"/tmp/xhs_publish_tab_debug.png"});const V=h?.ok?`clicked "${h.text}"`:`visible candidates: ${(h?.visibleTexts||[]).join(" | ")||"none"}`;throw Error("Still on the video publish page after trying to select 图文. "+`Details: ${V}. Debug screenshot: /tmp/xhs_publish_tab_debug.png`)}let f=b;if(O)f=await Nz(z,B,b);else{const V=await l(z,F);if(!V.ok){await z.screenshot({path:"/tmp/xhs_publish_upload_debug.png"});throw new H(`Image injection failed: ${V.error??"unknown"}. Debug screenshot: /tmp/xhs_publish_upload_debug.png`)}await z.wait({time:x/1000});await r(z)}if(!await yz(z)){await z.screenshot({path:"/tmp/xhs_publish_form_debug.png"});throw new H("Editing form did not appear after image acquisition. The page layout may have changed. Debug screenshot: /tmp/xhs_publish_form_debug.png")}if(O)await t(z,B.length,"文字配图 generated images");if(O&&F.length>0){const V=await l(z,F);if(!V.ok){await z.screenshot({path:"/tmp/xhs_publish_append_debug.png"});throw new H(`Appending images failed: ${V.error??"unknown"}. Debug screenshot: /tmp/xhs_publish_append_debug.png`)}await z.wait({time:x/1000});await r(z);await t(z,B.length+F.length,"文字配图 appended images")}await g(z,w,K,"title");await z.wait({time:0.5});await g(z,m,Q,"content");await z.wait({time:0.5});let P=[];if($.length)P=await Yz(z,m,$);const R=j?["暂存离开","存草稿"]:["发布","发布笔记"],G=await z.evaluate(`
510
510
  (cfg => {
511
511
  const { isDraftMode, publishNames, draftNames, labels } = cfg;
512
512
  const isVisible = (el) => {
@@ -545,8 +545,8 @@ import*as A from"node:fs";import*as U from"node:path";import{CommandExecutionErr
545
545
  }
546
546
  }
547
547
  return { ok: false, via: 'none', hosts: hosts.length, lastMethodError };
548
- })(${JSON.stringify({isDraftMode:$,publishNames:Zz,draftNames:$z,labels:M})})
549
- `);if(!G?.ok){if($){if(await z.evaluate(`
548
+ })(${JSON.stringify({isDraftMode:j,publishNames:qz,draftNames:jz,labels:R})})
549
+ `);if(!G?.ok){if(j){if(await z.evaluate(`
550
550
  (() => {
551
551
  const labels = ['返回', '关闭', '取消', '离开'];
552
552
  const buttons = document.querySelectorAll('button, [role="button"], div, span');
@@ -584,7 +584,7 @@ import*as A from"node:fs";import*as U from"node:path";import{CommandExecutionErr
584
584
  }
585
585
  return false;
586
586
  }
587
- `)){G.ok=!0;G.via="auto-save"}}}}if(!G?.ok){await z.screenshot({path:"/tmp/xhs_publish_submit_debug.png"});const V=G?.via?` (via=${G.via})`:"",k=G?.error?`, error=${G.error}`:"",_=G?.lastMethodError?`, lastMethodError=${G.lastMethodError}`:"";throw Error(`Could not trigger "${M[0]}" action${V}${k}${_}. Debug screenshot: /tmp/xhs_publish_submit_debug.png`)}await z.wait({time:4});const I=await z.evaluate("() => location.href"),a=$?["草稿已保存","暂存成功","保存成功","保存于","图文笔记("]:["发布成功","上传成功"],b=await z.evaluate(`
587
+ `)){G.ok=!0;G.via="auto-save"}}}}if(!G?.ok){await z.screenshot({path:"/tmp/xhs_publish_submit_debug.png"});const V=G?.via?` (via=${G.via})`:"",k=G?.error?`, error=${G.error}`:"",A=G?.lastMethodError?`, lastMethodError=${G.lastMethodError}`:"";throw Error(`Could not trigger "${R[0]}" action${V}${k}${A}. Debug screenshot: /tmp/xhs_publish_submit_debug.png`)}await z.wait({time:4});const N=await z.evaluate("() => location.href"),e=j?["草稿已保存","暂存成功","保存成功","保存于","图文笔记("]:["发布成功","上传成功"],_=await z.evaluate(`
588
588
  (markers => {
589
589
  for (const el of document.querySelectorAll('*')) {
590
590
  if (el.tagName === 'STYLE' || el.tagName === 'SCRIPT') continue;
@@ -593,5 +593,5 @@ import*as A from"node:fs";import*as U from"node:path";import{CommandExecutionErr
593
593
  if (el.children.length === 0 && markers.some(marker => text.includes(marker))) return text;
594
594
  }
595
595
  return '';
596
- })(${JSON.stringify(a)})
597
- `),e=!I.includes("/publish/publish"),zz=b.length>0||e,R=$?"暂存成功":"发布成功";if(!zz)throw new H(`${R} could not be verified: no success marker or post-submit navigation was observed. `+(I?`Current URL: ${I}`:"Current URL was empty."));return[{status:`✅ ${R}`,detail:[`"${K}"`,O?`${Y.length}张文字配图${F.length?` + ${F.length}张图片`:""}${f&&f!==B?` (${f})`:""}`:`${F.length}张图片`,N.length?`话题: ${N.join(" ")}`:"",b||I||""].filter(Boolean).join(" · ")}]}});
596
+ })(${JSON.stringify(e)})
597
+ `),zz=!N.includes("/publish/publish"),Jz=_.length>0||zz,S=j?"暂存成功":"发布成功";if(!Jz)throw new H(`${S} could not be verified: no success marker or post-submit navigation was observed. `+(N?`Current URL: ${N}`:"Current URL was empty."));return[{status:`✅ ${S}`,detail:[`"${K}"`,O?`${B.length}张文字配图${F.length?` + ${F.length}张图片`:""}${f&&f!==v?` (${f})`:""}`:`${F.length}张图片`,P.length?`话题: ${P.join(" ")}`:"",_||N||""].filter(Boolean).join(" · ")}]}});
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "publishport-opencli",
3
- "version": "1.8.5-pp.13",
3
+ "version": "1.8.5-pp.14",
4
4
  "publishConfig": {
5
5
  "access": "public",
6
6
  "provenance": false