publishport-opencli 1.8.5-pp.30 → 1.8.5-pp.32

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
@@ -18617,7 +18617,7 @@
18617
18617
  {
18618
18618
  "site": "imooc",
18619
18619
  "name": "article",
18620
- "description": "发布慕课手记文章(Markdown,始终存为草稿)。正文默认为 Markdown;外链图自动转存到慕课图床。",
18620
+ "description": "发布慕课手记文章(Markdown)。默认走「发表」(半自动:AI 填全部内容/栏目/标签/封面,最后的点选文字验证码需用户在弹出的浏览器窗口点一下);加 --draft 则只存草稿(无验证码)。外链图自动转存到慕课图床。",
18621
18621
  "access": "write",
18622
18622
  "domain": "www.imooc.com",
18623
18623
  "strategy": "cookie",
@@ -18628,14 +18628,14 @@
18628
18628
  "type": "str",
18629
18629
  "required": true,
18630
18630
  "positional": true,
18631
- "help": "文章标题"
18631
+ "help": "文章标题(≤80字)"
18632
18632
  },
18633
18633
  {
18634
18634
  "name": "text",
18635
18635
  "type": "str",
18636
18636
  "required": false,
18637
18637
  "positional": true,
18638
- "help": "文章正文(Markdown,默认)"
18638
+ "help": "文章正文(Markdown,默认;发表要求正文≥200字)"
18639
18639
  },
18640
18640
  {
18641
18641
  "name": "file",
@@ -18643,6 +18643,30 @@
18643
18643
  "required": false,
18644
18644
  "help": "正文文件路径(UTF-8,Markdown)"
18645
18645
  },
18646
+ {
18647
+ "name": "column",
18648
+ "type": "str",
18649
+ "required": false,
18650
+ "help": "栏目(发表必填):名称或 data-columnid,如「后端开发」/120、「前端开发」/119、「人工智能」/117"
18651
+ },
18652
+ {
18653
+ "name": "tags",
18654
+ "type": "str",
18655
+ "required": false,
18656
+ "help": "标签(发表必填,逗号分隔,至少 1 个):名称或 data-tag,如 MySQL,Java 或 11,3"
18657
+ },
18658
+ {
18659
+ "name": "cover",
18660
+ "type": "str",
18661
+ "required": false,
18662
+ "help": "封面图 URL 或本机路径(发表可选,缺省用慕课随机封面)"
18663
+ },
18664
+ {
18665
+ "name": "original",
18666
+ "type": "str",
18667
+ "required": false,
18668
+ "help": "文章类型(发表可选,默认「原创」):原创 / 首发 / 转载"
18669
+ },
18646
18670
  {
18647
18671
  "name": "html",
18648
18672
  "type": "boolean",
@@ -18653,7 +18677,7 @@
18653
18677
  "name": "draft",
18654
18678
  "type": "boolean",
18655
18679
  "required": false,
18656
- "help": "(慕课始终草稿,此标志无额外效果)"
18680
+ "help": "只存草稿(不发表,无需验证码)"
18657
18681
  },
18658
18682
  {
18659
18683
  "name": "execute",
@@ -17,6 +17,35 @@ var PP = (function () {
17
17
  for (var i = 0; i < parts.length; i++) { if (cur == null) return undefined; cur = cur[parts[i]]; }
18
18
  return cur;
19
19
  }
20
+ // 图片转存返回值必须是真 URL(http(s):// 或协议相对 //),否则视为上传失败。
21
+ // 防「HTTP 200 + 业务错误体」把 result:"服务器内部错误" 之类文本当 URL 写进正文。
22
+ function isHttpUrl(u) {
23
+ return typeof u === 'string' && /^(?:https?:)?\/\//i.test(u.trim());
24
+ }
25
+ // 有些图床按【文件名后缀】嗅探图片类型,缺后缀会直接 500(已确证开源中国如此,
26
+ // xueqiu 也靠 image.jpg 才成)。给 multipart 上传补一个真实后缀:优先 blob MIME,
27
+ // 其次源 URL 后缀,最后兜底 png。spec 已显式给出带后缀的 fileName 时尊重原值。
28
+ function extFromMime(mime) {
29
+ var map = { 'image/png': 'png', 'image/jpeg': 'jpg', 'image/jpg': 'jpg', 'image/gif': 'gif', 'image/webp': 'webp', 'image/bmp': 'bmp', 'image/svg+xml': 'svg', 'image/avif': 'avif', 'image/x-icon': 'ico', 'image/vnd.microsoft.icon': 'ico' };
30
+ return map[String(mime || '').toLowerCase().split(';')[0].trim()] || '';
31
+ }
32
+ function uploadFileName(base, src, mime) {
33
+ base = base || 'image';
34
+ if (/\.[a-z0-9]{2,5}$/i.test(base)) return base; // spec 已带后缀,原样用
35
+ var ext = extFromMime(mime);
36
+ if (!ext) {
37
+ var m = String(src || '').split(/[?#]/)[0].match(/\.([a-z0-9]{2,5})$/i);
38
+ if (m && /^(png|jpe?g|gif|webp|bmp|svg|avif|ico)$/i.test(m[1])) ext = m[1].toLowerCase() === 'jpeg' ? 'jpg' : m[1].toLowerCase();
39
+ }
40
+ return base + '.' + (ext || 'png');
41
+ }
42
+ // 业务层错误探测:端点常返 HTTP 200 但 body 里带 success:false / code>=400。
43
+ function bizError(json) {
44
+ if (!json || typeof json !== 'object') return null;
45
+ if (json.success === false) return String(json.message || json.result || 'success=false').slice(0, 150);
46
+ if (typeof json.code === 'number' && json.code >= 400) return 'code ' + json.code + ' ' + String(json.message || json.result || '').slice(0, 120);
47
+ return null;
48
+ }
20
49
 
21
50
  // ============ data: URI → Blob(不经 fetch)============
22
51
  // 不少平台页面的 CSP 会拦截 fetch("data:...")(connect-src 不含 data:,
@@ -619,7 +648,7 @@ var PP = (function () {
619
648
  } else if (bt === 'binary-multipart') {
620
649
  var blob = await (await fetch(src, { credentials: 'omit' })).blob();
621
650
  var fd = new FormData();
622
- fd.append(spec.fileField || 'file', blob, spec.fileName || 'image');
651
+ fd.append(spec.fileField || 'file', blob, uploadFileName(spec.fileName, src, blob.type));
623
652
  var extra = subst(spec.body || {}, src);
624
653
  for (var k in extra) if (Object.prototype.hasOwnProperty.call(extra, k)) fd.append(k, String(extra[k]));
625
654
  body = fd;
@@ -631,8 +660,11 @@ var PP = (function () {
631
660
  var txt = await resp.text();
632
661
  var json = null; try { json = JSON.parse(txt); } catch (e) {}
633
662
  if (!resp.ok) throw new Error('HTTP ' + resp.status + ' ' + txt.slice(0, 150));
663
+ var biz = bizError(json);
664
+ if (biz) throw new Error('图床返回业务错误: ' + biz);
634
665
  var newUrl = spec.responsePath ? pickPath(json, spec.responsePath) : (json && (json.url || json.src));
635
666
  if (!newUrl) throw new Error('no url in response: ' + txt.slice(0, 150));
667
+ if (!isHttpUrl(newUrl)) throw new Error('图床返回的不是合法 URL: ' + String(newUrl).slice(0, 150));
636
668
  cache[src] = newUrl;
637
669
  report.uploaded.push({ src: src.slice(0, 120), url: newUrl });
638
670
  } catch (e) {
@@ -674,6 +706,7 @@ var PP = (function () {
674
706
  var res = await uploadFn(src);
675
707
  var url = (res && typeof res === 'object') ? res.url : res;
676
708
  if (!url) throw new Error('uploadFn returned no url');
709
+ if (!isHttpUrl(url)) throw new Error('uploadFn 返回的不是合法 URL: ' + String(url).slice(0, 150));
677
710
  cache[src] = { url: url, attrs: (res && res.attrs) || null };
678
711
  report.uploaded.push({ src: src.slice(0, 120), url: url });
679
712
  } catch (e) {
@@ -697,6 +730,9 @@ var PP = (function () {
697
730
  xsrf: function () { return cookie('_xsrf'); },
698
731
  escapeHtml: escapeHtml,
699
732
  pickPath: pickPath,
733
+ isHttpUrl: isHttpUrl,
734
+ bizError: bizError,
735
+ uploadFileName: uploadFileName,
700
736
  md5: md5,
701
737
  dataUriToBlob: dataUriToBlob,
702
738
  webpToJpeg: webpToJpeg,
@@ -1 +1,59 @@
1
- import{CliError as M,CommandExecutionError as W}from"@jackwener/opencli/errors";import{cli as X,Strategy as Y}from"@jackwener/opencli/registry";import{publishArticle as Z}from"../_shared/article/publish.js";import{checkLogin as I}from"../_shared/article/auth.js";import{readFile as $,stat as q}from"node:fs/promises";export const imoocProfile={home:"https://www.imooc.com/article",outputFormat:"markdown",checkAuth:async(H)=>{let z=await(await fetch("https://www.imooc.com/u/card",{credentials:"include"})).text();z=z.replace(/^jsonpcallback\(/,"").replace(/\}\)$/,"}");let B;try{B=JSON.parse(z)}catch(G){return{isAuthenticated:!1,error:"JSONP 解析失败:"+String(G&&G.message||G)}}if(!B||B.result!==0)return{isAuthenticated:!1,error:B&&B.msg||"未登录"};return{isAuthenticated:!0,userId:String(B.data.uid),username:B.data.nickname,avatar:B.data.img}},image:{uploadFn:async(H,K)=>{const z=await fetch(H,{credentials:"omit"});if(!z.ok)throw Error("图片下载失败:HTTP "+z.status);const B=await z.blob(),G=Date.now()+".jpg",J=new File([B],G,{type:B.type||"image/jpeg"}),L=new FormData;L.append("photo",J,G);L.append("type",J.type);L.append("id","WU_FILE_0");L.append("name",G);L.append("lastModifiedDate",new Date().toString());L.append("size",String(J.size));const O=await(await fetch("https://www.imooc.com/article/ajaxuploadimg",{method:"POST",credentials:"include",body:L})).json();if(!O||O.result!==0)throw Error(O&&O.msg||"图片上传失败");let Q=O.data.imgpath;if(Q&&Q.indexOf("//")===0)Q="https:"+Q;return{url:Q}},skip:["img.imooc.com","imooc.com"]},publish:async(H,K)=>{const z=await fetch("https://www.imooc.com/article/savedraft",{method:"POST",credentials:"include",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:new URLSearchParams({editor:"0",draft_id:"0",title:H.title,content:H.content})});if(!z.ok){const J=await z.text();return{ok:!1,stage:"savedraft",status:z.status,message:J.slice(0,300)}}const B=await z.json();if(!B||!B.data){const J=JSON.stringify(B).slice(0,300);return{ok:!1,stage:"savedraft",status:z.status,message:"保存草稿失败:"+J}}const G=String(B.data);return{ok:!0,id:G,url:"https://www.imooc.com/article/draft/id/"+G,draft:!0}}};function A(H){if(!H.execute)throw new M("INVALID_INPUT","此命令需要 --execute 才会真正写入。注意:本报错仅表示缺少 --execute,并未校验其余参数,不能当 dry-run 预览用")}async function F(H){const K=typeof H.text==="string"?H.text:void 0,z=typeof H.file==="string"?H.file:void 0;if(K&&z)throw new M("INVALID_INPUT","<text> 和 --file 不能同时使用");let B=K??"";if(z){let G;try{G=await q(z)}catch{throw new M("INVALID_INPUT","文件不存在:"+z)}if(!G.isFile())throw new M("INVALID_INPUT","路径必须是可读文本文件:"+z);let J;try{J=await $(z)}catch{throw new M("INVALID_INPUT","文件读取失败:"+z)}try{B=new TextDecoder("utf-8",{fatal:!0}).decode(J)}catch{throw new M("INVALID_INPUT","文件不是有效 UTF-8:"+z)}}if(!B.trim())throw new M("INVALID_INPUT","正文不能为空");return B}function T(H,K,z,B,G={}){return[{status:"success",outcome:B,message:H,target_type:K,target:z,...G}]}X({site:"imooc",name:"article",access:"write",description:"发布慕课手记文章(Markdown,始终存为草稿)。正文默认为 Markdown;外链图自动转存到慕课图床。",domain:"www.imooc.com",strategy:Y.COOKIE,browser:!0,args:[{name:"title",positional:!0,required:!0,help:"文章标题"},{name:"text",positional:!0,help:"文章正文(Markdown,默认)"},{name:"file",help:"正文文件路径(UTF-8,Markdown)"},{name:"html",type:"boolean",help:"将正文视为原始 HTML 而非 Markdown"},{name:"draft",type:"boolean",help:"(慕课始终草稿,此标志无额外效果)"},{name:"execute",type:"boolean",help:"真正执行写入,不加此标志时拒绝写操作"}],columns:["status","outcome","message","target_type","target","created_target","created_url"],func:async(H,K)=>{if(!H)throw new W("慕课手记发布需要浏览器会话");A(K);const z=String(K.title??"").trim();if(!z)throw new M("INVALID_INPUT","文章标题不能为空");const B=await F(K),G=await Z(H,{title:z,body:B,format:K.html?"html":"markdown",draftOnly:!0,profile:imoocProfile}),J=G.images.uploaded.length|0,L=G.images.failed.length|0;let V="已保存慕课手记草稿";if(J||L)V+=`・图片:${J} 张已转存${L?`,${L} 张失败`:""}`;return T(V,"article","","draft",{created_target:"article:"+G.id,created_url:G.url})}});
1
+ import{CliError as z,CommandExecutionError as T}from"@jackwener/opencli/errors";import{cli as U,Strategy as N}from"@jackwener/opencli/registry";import{publishArticle as P,gotoWritePage as h}from"../_shared/article/publish.js";import{normalizeContent as I}from"../_shared/article/format.js";import{inlineLocalImages as R,isLocalImagePath as x,localImageToDataUri as C}from"../_shared/article/images.js";import{readFile as b,stat as E}from"node:fs/promises";export const imoocProfile={home:"https://www.imooc.com/article",outputFormat:"markdown",checkAuth:async(Q)=>{let G=await(await fetch("https://www.imooc.com/u/card",{credentials:"include"})).text();G=G.replace(/^jsonpcallback\(/,"").replace(/\}\)$/,"}");let V;try{V=JSON.parse(G)}catch(Y){return{isAuthenticated:!1,error:"JSONP 解析失败:"+String(Y&&Y.message||Y)}}if(!V||V.result!==0)return{isAuthenticated:!1,error:V&&V.msg||"未登录"};return{isAuthenticated:!0,userId:String(V.data.uid),username:V.data.nickname,avatar:V.data.img}},image:{uploadFn:async(Q,X)=>{const G=await fetch(Q,{credentials:"omit"});if(!G.ok)throw Error("图片下载失败:HTTP "+G.status);const V=await G.blob(),Y=Date.now()+".jpg",Z=new File([V],Y,{type:V.type||"image/jpeg"}),$=new FormData;$.append("photo",Z,Y);$.append("type",Z.type);$.append("id","WU_FILE_0");$.append("name",Y);$.append("lastModifiedDate",new Date().toString());$.append("size",String(Z.size));const H=await(await fetch("https://www.imooc.com/article/ajaxuploadimg",{method:"POST",credentials:"include",body:$})).json();if(!H||H.result!==0)throw Error(H&&H.msg||"图片上传失败");let K=H.data.imgpath;if(K&&K.indexOf("//")===0)K="https:"+K;return{url:K}},skip:["img.imooc.com","imooc.com"]},publish:async(Q,X)=>{const G=await fetch("https://www.imooc.com/article/savedraft",{method:"POST",credentials:"include",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:new URLSearchParams({editor:"0",draft_id:"0",title:Q.title,content:Q.content})});if(!G.ok){const Z=await G.text();return{ok:!1,stage:"savedraft",status:G.status,message:Z.slice(0,300)}}const V=await G.json();if(!V||!V.data){const Z=JSON.stringify(V).slice(0,300);return{ok:!1,stage:"savedraft",status:G.status,message:"保存草稿失败:"+Z}}const Y=String(V.data);return{ok:!0,id:Y,url:"https://www.imooc.com/article/draft/id/"+Y,draft:!0}}};function k(Q){if(!Q.execute)throw new z("INVALID_INPUT","此命令需要 --execute 才会真正写入。注意:本报错仅表示缺少 --execute,并未校验其余参数,不能当 dry-run 预览用")}async function f(Q){const X=typeof Q.text==="string"?Q.text:void 0,G=typeof Q.file==="string"?Q.file:void 0;if(X&&G)throw new z("INVALID_INPUT","<text> 和 --file 不能同时使用");let V=X??"";if(G){let Y;try{Y=await E(G)}catch{throw new z("INVALID_INPUT","文件不存在:"+G)}if(!Y.isFile())throw new z("INVALID_INPUT","路径必须是可读文本文件:"+G);let Z;try{Z=await b(G)}catch{throw new z("INVALID_INPUT","文件读取失败:"+G)}try{V=new TextDecoder("utf-8",{fatal:!0}).decode(Z)}catch{throw new z("INVALID_INPUT","文件不是有效 UTF-8:"+G)}}if(!V.trim())throw new z("INVALID_INPUT","正文不能为空");return V}function _(Q,X,G,V,Y={}){return[{status:"success",outcome:V,message:Q,target_type:X,target:G,...Y}]}const w="https://www.imooc.com/article/publish",d="https?://www\\.imooc\\.com/article/(\\d+)",n=imoocProfile.image.uploadFn.toString();function c(Q,X){return`(async () => {
2
+ const I = `+JSON.stringify(Q)+`;
3
+ const __upload = (`+X+`);
4
+ const PP = {};
5
+ const sleep = (ms) => new Promise(r => setTimeout(r, ms));
6
+ var mk = document.querySelector('.js-change-editor[data-type="0"]');
7
+ if (mk && !mk.classList.contains("active")) { mk.click(); await sleep(500); }
8
+ for (var i = 0; i < 60 && !(window.app && typeof window.app.setEditorContent === "function"); i++) { await sleep(200); }
9
+ `+`if (!(window.app && typeof window.app.setEditorContent === "function")) return { ok: false, stage: "editor", message: "Markdown 编辑器未就绪" };
10
+ `+`var skip = I.skip || [];
11
+ function keep(u) { if (!u) return true; if (u.indexOf("data:") === 0) return false; if (/(^|\\/\\/)([^\\/]*\\.)?imooc\\.com/.test(u)) return true; return skip.some(function(s){ return u.indexOf(s) >= 0; }); }
12
+ var md = I.md, uploaded = [], failed = [];
13
+ var urls = new Set(), m;
14
+ var re1 = /!\\[[^\\]]*\\]\\(\\s*<?([^)\\s>]+)>?[^)]*\\)/g;
15
+ while ((m = re1.exec(md))) urls.add(m[1]);
16
+ var re2 = /<img[^>]+src=["']([^"']+)["']/gi;
17
+ while ((m = re2.exec(md))) urls.add(m[1]);
18
+ `+`for (var u of urls) { if (keep(u)) continue; try { var r = await __upload(u, PP); if (r && r.url) { md = md.split(u).join(r.url); uploaded.push({ src: u, url: r.url }); } else { failed.push({ src: u, error: "转存无返回 URL" }); } } catch (e) { failed.push({ src: u, error: String(e && e.message || e) }); } }
19
+ `+`var cover = "";
20
+ `+`if (I.cover) { if (keep(I.cover)) { cover = I.cover; } else { try { var rc = await __upload(I.cover, PP); if (rc && rc.url) cover = rc.url; else return { ok: false, stage: "cover", message: "封面图转存无返回 URL", uploaded: uploaded, failed: failed }; } catch (e) { return { ok: false, stage: "cover", message: "封面图转存失败:" + String(e && e.message || e), uploaded: uploaded, failed: failed }; } } }
21
+ `+`document.getElementById("article_title").value = I.title;
22
+ window.app.setEditorContent(md);
23
+ await sleep(400);
24
+ var plen = (window.parsinCtx && window.parsinCtx.text) ? window.parsinCtx.text.length : 0;
25
+ return { ok: true, uploaded: uploaded, failed: failed, cover: cover, parsinLen: plen };
26
+ })()`}function p(Q){return`(async () => {
27
+ const I = `+JSON.stringify(Q)+`;
28
+ const sleep = (ms) => new Promise(r => setTimeout(r, ms));
29
+ var pub = document.querySelector(".js-article-publish");
30
+ `+`if (!pub) return { ok: false, message: "找不到「发表」按钮" };
31
+ `+`pub.click();
32
+ await sleep(600);
33
+ var modal = document.getElementById("article_layer");
34
+ if (!modal || getComputedStyle(modal).display === "none") {
35
+ var tip = document.querySelector(".layer_prompt, .prompt-msg, .imooc-prompt, .layer_content");
36
+ `+` return { ok: false, message: (tip && tip.textContent.trim()) || "发表校验未通过(标题需≤80字、正文需≥200字)" };
37
+ `+`}
38
+ if (I.cover) { var pic = document.getElementById("article_pic"); pic.style.backgroundImage = "url(" + I.cover + ")"; pic.setAttribute("data-pic", I.cover); var ic = pic.querySelector("i"); if (ic) ic.style.display = "none"; }
39
+ else { var rnd = document.getElementById("pic_random_btn"); if (rnd) rnd.click(); }
40
+ var cols = Array.from(document.querySelectorAll(".js-column-box li[data-columnid]"));
41
+ var col = cols.find(function(li){ return li.getAttribute("data-columnid") === String(I.column) || li.textContent.trim() === String(I.column); });
42
+ `+`if (!col) return { ok: false, message: "未找到栏目「" + I.column + "」,可选:" + cols.map(function(l){ return l.textContent.trim(); }).join("、") };
43
+ `+`col.click();
44
+ var origs = Array.from(document.querySelectorAll(".js-original-box li"));
45
+ var ot;
46
+ `+`if (I.original === "首发") ot = document.querySelector(".js-original-box li#publish");
47
+ `+`else if (I.original === "转载" || I.original === "其他") ot = origs.find(function(l){ return l.getAttribute("data-original") === "0"; });
48
+ `+`else ot = origs.find(function(l){ return l.getAttribute("data-original") === "1" && l.id !== "publish"; });
49
+ if (ot) ot.click();
50
+ var all = Array.from(document.querySelectorAll(".js-tag-list .tag-item"));
51
+ var chosen = [], notFound = [];
52
+ for (var k of I.tags) { var t = all.find(function(x){ return x.getAttribute("data-tag") === String(k) || x.textContent.trim().toLowerCase() === String(k).toLowerCase(); }); if (t) { t.click(); chosen.push(t.textContent.trim()); } else { notFound.push(k); } }
53
+ var chosenCount = document.querySelectorAll(".js-chosen-tags .tag-item").length;
54
+ `+`if (chosenCount === 0) return { ok: false, message: "标签一个都没匹配到:" + I.tags.join(",") + "。用 data-tag 或标签名,如 MySQL / Java / Python / Go / 前端工具" };
55
+ `+`try { window.addEventListener("beforeunload", function(e){ e.stopImmediatePropagation(); if (e.preventDefault) e.preventDefault(); try { delete e["returnValue"]; } catch (x) {} }, true); window.onbeforeunload = null; } catch (x) {}
56
+ document.getElementById("save_article").click();
57
+ await sleep(400);
58
+ return { ok: true, chosen: chosen, notFound: notFound, column: col.textContent.trim() };
59
+ })()`}async function m(Q,X){const{title:G,body:V,format:Y,column:Z,tags:$,original:B,cover:H}=X,K=I(V,{format:Y}),F=await R(K.markdown);let W=F.content;const A=F.missing||[];let J="";if(H){const M=H.trim();if(/["<>\s]/.test(M))throw new T("封面图路径/URL 含非法字符(引号/尖括号/空白):"+M.slice(0,120));if(x(M))try{J=(await C(M)).dataUri}catch(O){throw new T("封面图读取失败:"+M+"("+String(O&&O.message||O)+")")}else J=M}await h(Q,w,imoocProfile.originRe);const q=await Q.evaluate(c({title:G,md:W,cover:J,skip:imoocProfile.image.skip||[]},n));if(!q||q.ok===!1)throw new T("["+(q?.stage||"setup")+"] "+(q?.message||"内容准备失败"));if(!q.parsinLen||q.parsinLen<200)throw new T("正文进入编辑器后不足 200 字(慕课发表要求正文≥200字),当前约 "+(q.parsinLen||0)+" 字。");const j=await Q.evaluate(p({column:String(Z),tags:$,original:B,cover:q.cover}));if(!j||j.ok===!1)throw new T(j?.message||"填写发表信息失败");const v=new RegExp(d);let L="";for(let M=0;M<75;M++){await Q.wait({time:2});let O="";try{O=await Q.getCurrentUrl()}catch{}const y=O&&O.match(v);if(y){L=y[1];break}}const D=q.uploaded||[],S=(q.failed||[]).concat(A);if(!L)throw new T("内容与发表信息已全部填好并进入发表流程,但未检测到发表成功跳转"+"(约 150 秒内未完成点选文字验证码)。请在弹出的慕课浏览器窗口按提示点选文字完成验证,"+"页面会自动跳到文章页即代表发表成功;或稍后重试本命令。");return{id:L,url:"https://www.imooc.com/article/"+L,images:{uploaded:D,failed:S},column:j.column,tags:j.chosen||[],notFoundTags:j.notFound||[]}}U({site:"imooc",name:"article",access:"write",description:"发布慕课手记文章(Markdown)。默认走「发表」(半自动:AI 填全部内容/栏目/标签/封面,"+"最后的点选文字验证码需用户在弹出的浏览器窗口点一下);加 --draft 则只存草稿(无验证码)。外链图自动转存到慕课图床。",domain:"www.imooc.com",strategy:N.COOKIE,browser:!0,args:[{name:"title",positional:!0,required:!0,help:"文章标题(≤80字)"},{name:"text",positional:!0,help:"文章正文(Markdown,默认;发表要求正文≥200字)"},{name:"file",help:"正文文件路径(UTF-8,Markdown)"},{name:"column",help:"栏目(发表必填):名称或 data-columnid,如「后端开发」/120、「前端开发」/119、「人工智能」/117"},{name:"tags",help:"标签(发表必填,逗号分隔,至少 1 个):名称或 data-tag,如 MySQL,Java 或 11,3"},{name:"cover",help:"封面图 URL 或本机路径(发表可选,缺省用慕课随机封面)"},{name:"original",help:"文章类型(发表可选,默认「原创」):原创 / 首发 / 转载"},{name:"html",type:"boolean",help:"将正文视为原始 HTML 而非 Markdown"},{name:"draft",type:"boolean",help:"只存草稿(不发表,无需验证码)"},{name:"execute",type:"boolean",help:"真正执行写入,不加此标志时拒绝写操作"}],columns:["status","outcome","message","target_type","target","created_target","created_url"],func:async(Q,X)=>{if(!Q)throw new T("慕课手记发布需要浏览器会话");k(X);const G=String(X.title??"").trim();if(!G)throw new z("INVALID_INPUT","文章标题不能为空");if(G.length>80)throw new z("INVALID_INPUT","标题不能超过 80 字,当前 "+G.length+" 字");const V=await f(X);if(X.draft){const W=await P(Q,{title:G,body:V,format:X.html?"html":"markdown",draftOnly:!0,profile:imoocProfile}),A=W.images.uploaded.length|0,J=W.images.failed.length|0;let q="已保存慕课手记草稿";if(A||J)q+=`・图片:${A} 张已转存${J?`,${J} 张失败`:""}`;return _(q,"article","","draft",{created_target:"article:"+W.id,created_url:W.url})}const Y=String(X.column??"").trim();if(!Y)throw new z("INVALID_INPUT","发表必填 --column(栏目)。可选:前端开发/后端开发/人工智能/对话ChatGPT/移动开发/云计算大数据/产品设计/工具资源/职场生活/经验分享/其它。"+"只想存草稿加 --draft。");const Z=String(X.tags??"").split(",").map((W)=>W.trim()).filter(Boolean);if(Z.length===0)throw new z("INVALID_INPUT","发表必填 --tags(标签,逗号分隔,至少 1 个),如 --tags MySQL,Java。只想存草稿加 --draft。");const $=String(X.original??"原创").trim()||"原创";if(!["原创","首发","转载","其他"].includes($))throw new z("INVALID_INPUT","--original 只能是 原创 / 首发 / 转载,当前:"+$);const B=await m(Q,{title:G,body:V,format:X.html?"html":"markdown",column:Y,tags:Z,original:$,cover:typeof X.cover==="string"?X.cover:""}),H=B.images.uploaded.length|0,K=B.images.failed.length|0;let F=`已发表慕课手记・栏目「${B.column}」・标签 ${B.tags.join("/")}`;if(H||K)F+=`・图片:${H} 张已转存${K?`,${K} 张失败`:""}`;if(B.notFoundTags.length)F+=`・未匹配标签:${B.notFoundTags.join(",")}`;return _(F,"article","","published",{created_target:"article:"+B.id,created_url:B.url})}});
@@ -1 +1 @@
1
- import{CliError as V,CommandExecutionError as A}from"@jackwener/opencli/errors";import{cli as F,Strategy as O}from"@jackwener/opencli/registry";import{readFile as _,stat as q}from"node:fs/promises";import{publishArticle as T}from"../_shared/article/publish.js";export const oschinaProfile={home:"https://my.oschina.net",outputFormat:"markdown",image:{spec:{url:"https://apiv1.oschina.net/oschinapi/ai/creation/project/uploadDetail",method:"POST",bodyType:"binary-multipart",fileField:"file",fileName:"image",body:{},responsePath:"result"},skip:["apiv1.oschina.net","oscimg.oschina.net","static.oschina.net"]},checkAuth:async()=>{const G=await fetch("https://apiv1.oschina.net/oschinapi/user/myDetails",{credentials:"include"});if(!G.ok)return{isAuthenticated:!1,error:"HTTP "+G.status};let z=null;try{z=await G.json()}catch(U){return{isAuthenticated:!1,error:"解析响应失败"}}if(!z||!z.success||!z.result||!z.result.userId)return{isAuthenticated:!1,error:z&&z.message||"未登录"};const B=String(z.result.userId),M=z.result.userVo&&z.result.userVo.name||B,Q=z.result.userVo&&z.result.userVo.portraitUrl||"";return{isAuthenticated:!0,userId:B,username:M,avatar:Q}},publish:async(G)=>{const z=G.params||{};let B="";try{const L=await(await fetch("https://apiv1.oschina.net/oschinapi/user/myDetails",{credentials:"include"})).json();if(L&&L.success&&L.result&&L.result.userId)B=String(L.result.userId)}catch(K){}if(!B)return{ok:!1,stage:"auth",status:401,message:"未登录开源中国,无法获取用户 ID"};let M=0;if(z.category){let K=[];try{const X=await(await fetch("https://apiv1.oschina.net/oschinapi/blog_catalog/list_by_user",{credentials:"include"})).json();K=X&&X.result||[]}catch(H){return{ok:!1,stage:"category",message:"获取开源中国博客分类失败:"+String(H&&H.message||H)}}const L=K.find((H)=>String(H.name)===String(z.category));if(!L)return{ok:!1,stage:"category",message:"未找到博客分类「"+z.category+"」,可选:"+(K.map((H)=>H.name).join(" / ")||"(你还没有任何分类)")};M=L.id}if(G.draftOnly){const K=await fetch("https://apiv1.oschina.net/oschinapi/api/draft/save_draft",{method:"POST",credentials:"include",headers:{"Content-Type":"application/json"},body:JSON.stringify({title:G.title,user:Number(B),content:G.content,contentType:1,catalog:M,originUrl:"",privacy:!0,disableComment:!1})}),L=await K.text();let H=null;try{H=JSON.parse(L)}catch(S){}if(!K.ok||!H||!H.success||!H.result||!H.result.id)return{ok:!1,stage:"save_draft",status:K.status,message:H&&H.message||L.slice(0,300)};const X=String(H.result.id);return{ok:!0,draft:!0,id:X,url:"https://my.oschina.net/u/"+B+"/blog/write/draft/"+X}}const Q=await fetch("https://apiv1.oschina.net/oschinapi/blog/web/add",{method:"POST",credentials:"include",headers:{"Content-Type":"application/json"},body:JSON.stringify({title:G.title,content:G.content,contentType:1,type:"1",originUrl:"",catalog:M,privacy:!1,disableComment:!1,isAiRelated:!1,user:Number(B)})}),U=await Q.text();let J=null;try{J=JSON.parse(U)}catch(K){}if(!Q.ok||!J||J.code!==200||J.result==null)return{ok:!1,stage:"publish",status:Q.status,message:J&&(J.message||J.msg)||U.slice(0,300)};const W=String(J.result);return{ok:!0,draft:!1,id:W,url:"https://my.oschina.net/u/"+B+"/blog/"+W}}};function Y(G){if(!G.execute)throw new V("INVALID_INPUT","发布开源中国文章需要 --execute 参数。注意:本报错仅表示缺少 --execute,并未校验其余参数,不能当 dry-run 预览用")}function Z(G,z,B,M,Q={}){return[{status:"success",outcome:M,message:G,target_type:z,target:B,...Q}]}async function $(G){const z=typeof G.text==="string"?G.text:void 0,B=typeof G.file==="string"?G.file:void 0;if(z&&B)throw new V("INVALID_INPUT","正文和文件路径只能二选一,不能同时指定");let M=z??"";if(B){let Q;try{Q=await q(B)}catch{throw new V("INVALID_INPUT","文件不存在:"+B)}if(!Q.isFile())throw new V("INVALID_INPUT","必须指定可读的文本文件:"+B);let U;try{U=await _(B)}catch{throw new V("INVALID_INPUT","文件读取失败:"+B)}try{M=new TextDecoder("utf-8",{fatal:!0}).decode(U)}catch{throw new V("INVALID_INPUT","文件编码不是 UTF-8:"+B)}}if(!M.trim())throw new V("INVALID_INPUT","文章正文不能为空");return M}F({site:"oschina",name:"article",access:"write",description:"发布文章到开源中国博客。默认正式发布,加 --draft 仅存草稿。正文默认 Markdown,外链图片自动转存到开源中国图床。",domain:"my.oschina.net",strategy:O.COOKIE,browser:!0,args:[{name:"title",positional:!0,required:!0,help:"文章标题"},{name:"text",positional:!0,help:"文章正文(默认 Markdown;--html 表示原始 HTML)"},{name:"file",help:"从文件读取正文(UTF-8,默认 Markdown)"},{name:"html",type:"boolean",help:"将正文视为原始 HTML 而非 Markdown"},{name:"category",help:"个人博客分类名(精确匹配),合法值用 `oschina catalogs` 列举;可空=不分类"},{name:"draft",type:"boolean",help:"仅存草稿,不发布"},{name:"execute",type:"boolean",help:"真正执行写操作;不加此参数命令拒绝写入"}],columns:["status","outcome","message","target_type","target","created_target","created_url"],func:async(G,z)=>{if(!G)throw new A("开源中国文章发布需要浏览器会话");Y(z);const B=String(z.title??"").trim();if(!B)throw new V("INVALID_INPUT","文章标题不能为空");const M=await $(z),Q=Boolean(z.draft),U={category:typeof z.category==="string"?z.category.trim():""},J=await T(G,{title:B,body:M,format:z.html?"html":"markdown",draftOnly:Q,profile:oschinaProfile,publishParams:U}),W=J.images.uploaded.length|0,K=J.images.failed.length|0;let L=J.draft?"已保存到开源中国草稿箱":"已正式发布到开源中国博客";if(W||K)L+=`·图片:${W} 张已转存${K?`,${K} 张失败`:""}`;return Z(L,"article","",J.draft?"draft":"created",{created_target:(J.draft?"draft:":"article:")+J.id,created_url:J.url})}});export const __test__={oschinaProfile,requireExecute:Y,buildResultRow:Z,resolvePayload:$};
1
+ import{CliError as V,CommandExecutionError as A}from"@jackwener/opencli/errors";import{cli as F,Strategy as O}from"@jackwener/opencli/registry";import{readFile as _,stat as q}from"node:fs/promises";import{publishArticle as T}from"../_shared/article/publish.js";export const oschinaProfile={home:"https://my.oschina.net",outputFormat:"markdown",image:{spec:{url:"https://apiv1.oschina.net/oschinapi/ai/creation/project/uploadDetail",method:"POST",bodyType:"binary-multipart",fileField:"file",fileName:"image",body:{},responsePath:"result"},skip:["apiv1.oschina.net","oscimg.oschina.net","static.oschina.net"]},checkAuth:async()=>{const G=await fetch("https://apiv1.oschina.net/oschinapi/user/myDetails",{credentials:"include"});if(!G.ok)return{isAuthenticated:!1,error:"HTTP "+G.status};let z=null;try{z=await G.json()}catch(U){return{isAuthenticated:!1,error:"解析响应失败"}}if(!z||!z.success||!z.result||!z.result.userId)return{isAuthenticated:!1,error:z&&z.message||"未登录"};const B=String(z.result.userId),M=z.result.userVo&&z.result.userVo.name||B,Q=z.result.userVo&&z.result.userVo.portraitUrl||"";return{isAuthenticated:!0,userId:B,username:M,avatar:Q}},publish:async(G)=>{const z=G.params||{};let B="";try{const L=await(await fetch("https://apiv1.oschina.net/oschinapi/user/myDetails",{credentials:"include"})).json();if(L&&L.success&&L.result&&L.result.userId)B=String(L.result.userId)}catch(K){}if(!B)return{ok:!1,stage:"auth",status:401,message:"未登录开源中国,无法获取用户 ID"};let M=0;if(z.category){let K=[];try{const X=await(await fetch("https://apiv1.oschina.net/oschinapi/blog_catalog/list_by_user",{credentials:"include"})).json();K=X&&X.result||[]}catch(H){return{ok:!1,stage:"category",message:"获取开源中国博客分类失败:"+String(H&&H.message||H)}}const L=K.find((H)=>String(H.name)===String(z.category));if(!L)return{ok:!1,stage:"category",message:"未找到博客分类「"+z.category+"」,可选:"+(K.map((H)=>H.name).join(" / ")||"(你还没有任何分类)")};M=L.id}if(G.draftOnly){const K=await fetch("https://apiv1.oschina.net/oschinapi/api/draft/save_draft",{method:"POST",credentials:"include",headers:{"Content-Type":"application/json"},body:JSON.stringify({title:G.title,user:Number(B),content:G.content,contentType:1,catalog:M,originUrl:"",privacy:!0,disableComment:!1})}),L=await K.text();let H=null;try{H=JSON.parse(L)}catch(S){}if(!K.ok||!H||!H.success||!H.result||!H.result.id)return{ok:!1,stage:"save_draft",status:K.status,message:H&&H.message||L.slice(0,300)};const X=String(H.result.id);return{ok:!0,draft:!0,id:X,url:"https://my.oschina.net/u/"+B+"/blog/ai-write/draft/"+X}}const Q=await fetch("https://apiv1.oschina.net/oschinapi/blog/web/add",{method:"POST",credentials:"include",headers:{"Content-Type":"application/json"},body:JSON.stringify({title:G.title,content:G.content,contentType:1,type:"1",originUrl:"",catalog:M,privacy:!1,disableComment:!1,isAiRelated:!1,user:Number(B)})}),U=await Q.text();let J=null;try{J=JSON.parse(U)}catch(K){}if(!Q.ok||!J||J.code!==200||J.result==null)return{ok:!1,stage:"publish",status:Q.status,message:J&&(J.message||J.msg)||U.slice(0,300)};const W=String(J.result);return{ok:!0,draft:!1,id:W,url:"https://my.oschina.net/u/"+B+"/blog/"+W}}};function Y(G){if(!G.execute)throw new V("INVALID_INPUT","发布开源中国文章需要 --execute 参数。注意:本报错仅表示缺少 --execute,并未校验其余参数,不能当 dry-run 预览用")}function Z(G,z,B,M,Q={}){return[{status:"success",outcome:M,message:G,target_type:z,target:B,...Q}]}async function $(G){const z=typeof G.text==="string"?G.text:void 0,B=typeof G.file==="string"?G.file:void 0;if(z&&B)throw new V("INVALID_INPUT","正文和文件路径只能二选一,不能同时指定");let M=z??"";if(B){let Q;try{Q=await q(B)}catch{throw new V("INVALID_INPUT","文件不存在:"+B)}if(!Q.isFile())throw new V("INVALID_INPUT","必须指定可读的文本文件:"+B);let U;try{U=await _(B)}catch{throw new V("INVALID_INPUT","文件读取失败:"+B)}try{M=new TextDecoder("utf-8",{fatal:!0}).decode(U)}catch{throw new V("INVALID_INPUT","文件编码不是 UTF-8:"+B)}}if(!M.trim())throw new V("INVALID_INPUT","文章正文不能为空");return M}F({site:"oschina",name:"article",access:"write",description:"发布文章到开源中国博客。默认正式发布,加 --draft 仅存草稿。正文默认 Markdown,外链图片自动转存到开源中国图床。",domain:"my.oschina.net",strategy:O.COOKIE,browser:!0,args:[{name:"title",positional:!0,required:!0,help:"文章标题"},{name:"text",positional:!0,help:"文章正文(默认 Markdown;--html 表示原始 HTML)"},{name:"file",help:"从文件读取正文(UTF-8,默认 Markdown)"},{name:"html",type:"boolean",help:"将正文视为原始 HTML 而非 Markdown"},{name:"category",help:"个人博客分类名(精确匹配),合法值用 `oschina catalogs` 列举;可空=不分类"},{name:"draft",type:"boolean",help:"仅存草稿,不发布"},{name:"execute",type:"boolean",help:"真正执行写操作;不加此参数命令拒绝写入"}],columns:["status","outcome","message","target_type","target","created_target","created_url"],func:async(G,z)=>{if(!G)throw new A("开源中国文章发布需要浏览器会话");Y(z);const B=String(z.title??"").trim();if(!B)throw new V("INVALID_INPUT","文章标题不能为空");const M=await $(z),Q=Boolean(z.draft),U={category:typeof z.category==="string"?z.category.trim():""},J=await T(G,{title:B,body:M,format:z.html?"html":"markdown",draftOnly:Q,profile:oschinaProfile,publishParams:U}),W=J.images.uploaded.length|0,K=J.images.failed.length|0;let L=J.draft?"已保存到开源中国草稿箱":"已正式发布到开源中国博客";if(W||K)L+=`·图片:${W} 张已转存${K?`,${K} 张失败`:""}`;return Z(L,"article","",J.draft?"draft":"created",{created_target:(J.draft?"draft:":"article:")+J.id,created_url:J.url})}});export const __test__={oschinaProfile,requireExecute:Y,buildResultRow:Z,resolvePayload:$};
@@ -1,9 +1,9 @@
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(`
1
+ import*as y from"node:fs";import*as B from"node:path";import{CommandExecutionError as H,ArgumentError as X}from"@jackwener/opencli/errors";import{cli as Qz,Strategy as Zz}from"@jackwener/opencli/registry";import{assertLiteralContent as $z}from"../_shared/content-guard.js";const qz="https://creator.xiaohongshu.com/publish/publish?from=menu_left&target=image",C=9,T=20,x=3000,u="|||",D="基础",d=[["基础","默认兜底,万能"],["边框","金句/要点卡"],["备忘","提醒/随手记"],["清新","日常/清单贴士"],["涂写","随笔/碎碎念"],["便签","笔记/提醒"],["光影","情绪/文艺"],["涂鸦","趣味/童话"],["简约","干货/观点"],["手写","日记/情感"],["插图","生活方式/轻松话题"],["美漫","活力/趣味/故事感"],["弥散","弥散光氛围"],["柔和","柔和/温柔金句"],["印刷","印刷海报/排版"],["科技","科技/产品"],["贺卡","节日祝福"],["札记","艺术/水彩氛围"],["书摘","书摘/引用"],["手帐","手帐拼贴"],["几何","醒目/有力主张"]],_z=d.map(([z])=>z),L="文字配图",c="再写一张",E="生成图片",n="下一步",M=".tiptap.ProseMirror",jz=["_onPublish","onPublish","_onSubmit","_handlePublish"],Vz=["_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 Y(z){if(z&&typeof z==="object"&&typeof z.session==="string"&&Object.prototype.hasOwnProperty.call(z,"data"))return z.data;return z}function Hz(z){return z.map((J)=>{const K=B.resolve(J);if(!y.existsSync(K))throw new X(`Image file not found: ${K}`);const Z=B.extname(K).toLowerCase();if(!i[Z])throw new X(`Unsupported image format "${Z}". Supported: jpg, png, gif, webp`);return K})}const l='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 Gz(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(o)};
3
+ const sels = ${JSON.stringify(l)};
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 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(`
6
+ `))return!0;if(Q<Z-1)await z.wait({time:K/1000})}return!1}async function o(z,J){if(!await Gz(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,l);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=B.extname($).toLowerCase();return{name:B.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
8
  const images = ${q};
9
9
 
@@ -167,7 +167,7 @@ import*as y 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 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(`
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 Yz(z,J){return Y(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 y 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 _z(z,{click:J=!1}={}){return`
186
+ `))}function Az(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);
@@ -272,14 +272,14 @@ import*as y 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 Az(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 Cz(z,J){const K=`#${J}`;if(typeof z.insertText==="function")try{await z.insertText(K);return!0}catch{}return Y(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 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(`
282
+ `))}async function Xz(z,J,K){const Z=[];for(const Q of K){if(!await Yz(z,J))throw new H(`Could not attach topic "${Q}": body editor not found`);const $=Number(Y(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(Y(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 Wz(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 y 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 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(`
331
+ `);if(J?.ok)await z.wait({time:1});return J}async function f(z,J,K=7000){const Z=500,Q=Math.max(1,Math.ceil(K/Z));let q={ok:!1};for(let $=0;$<Q;$++){q=Y(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 y from"node:fs";import*as U from"node:path";import{CommandExecutionErr
363
363
  }
364
364
  return { ok: false };
365
365
  })()
366
- `));if(q?.ok)return q;await z.wait({time:Z/1000})}return q}async function Wz(z){const J=await z.evaluate(`
366
+ `));if(q?.ok)return q;await z.wait({time:Z/1000})}return q}async function Fz(z){const J=await z.evaluate(`
367
367
  (() => {
368
368
  const __opencli_xhs_focus_card = true;
369
- const sel = ${JSON.stringify(L)};
369
+ const sel = ${JSON.stringify(M)};
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 y 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 Fz(z){const J=await z.evaluate(`
383
+ `);return Y(J)}async function Oz(z){const J=await z.evaluate(`
384
384
  (() => {
385
385
  const __opencli_xhs_card_text = true;
386
- const sel = ${JSON.stringify(L)};
386
+ const sel = ${JSON.stringify(M)};
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 p(z){const J=await z.evaluate(`
392
+ `);return Y(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(L)};
395
+ const sel = ${JSON.stringify(M)};
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 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(`
401
+ `);return Y(J)}async function Uz(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 Bz(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 vz(z,J,K=4){for(let Z=0;Z<K;Z++){if(!(await f(z,c))?.ok)return!1;if(await Bz(z,J,2500))return!0}return!1}async function Dz(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 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(`
408
+ `);return Y(J)}async function fz(z,J=6){const K=400,Z=Math.ceil(3000/K);for(let Q=0;Q<J;Q++){if(!(await f(z,E))?.ok&&Q===0)return!1;for(let $=0;$<Z;$++){if((await Dz(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 Fz(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 Oz(z))?.ok)throw new H(`文字配图: card editor #${K+1} is empty after typing`)}async function Nz(z,J){if(!J||J===D)return D;const K=Y(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 y 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 ${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(`
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 ${D}.`);if(!(await f(z,J))?.ok)throw new H(`文字配图: could not click requested style "${J}".`);await z.wait({time:0.6});return J}async function t(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 y 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 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(`
471
+ `);return Y(J)}async function a(z,J,K){const Z=await t(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 hz(z,J,K){if(!(await f(z,L))?.ok){await z.screenshot({path:"/tmp/xhs_publish_textimage_debug.png"});throw new H(`文字配图: could not click "${L}" entry. Debug: /tmp/xhs_publish_textimage_debug.png`)}if(!await Uz(z)){await z.screenshot({path:"/tmp/xhs_publish_textimage_debug.png"});throw new H(`文字配图: 写文字 card editor did not appear after clicking "${L}". Debug: /tmp/xhs_publish_textimage_debug.png`)}for(let j=0;j<J.length;j++){if(j>0){if(!await vz(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 "${c}". Debug: /tmp/xhs_publish_addcard_debug.png`)}}await Iz(z,J[j],j)}if(!await fz(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 Nz(z,K);if(!(await f(z,n))?.ok){await z.screenshot({path:"/tmp/xhs_publish_next_debug.png"});throw new H(`文字配图: could not click "${n}". Debug: /tmp/xhs_publish_next_debug.png`)}await z.wait({time:2});return q}async function e(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 y 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 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(`
501
+ `)}async function kz(z,J=5000){const K=500,Z=Math.max(1,Math.ceil(J/K));let Q=await e(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 e(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,33 @@ import*as y 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}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(`
509
+ })()`))return!0;if(Q<Z-1)await z.wait({time:K/1000})}return!1}async function Pz(z){const J=async()=>Y(await z.evaluate(`
510
+ (() => {
511
+ const __opencli_xhs_restore_probe = true;
512
+ const clearBtn = document.querySelector('.title-clear-all');
513
+ const restored = !!clearBtn && clearBtn.offsetParent !== null && !!document.querySelector('.img-container');
514
+ const cleanSurface = (document.body.innerText || '').includes('上传图片,或写文字生成图片');
515
+ return { restored, cleanSurface };
516
+ })()
517
+ `));let K=await J();if(!K?.restored)return!1;for(let Z=0;Z<3;Z++){await z.evaluate(`
518
+ (async () => {
519
+ const __opencli_xhs_restore_clear = true;
520
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
521
+ const fire = (el) => {
522
+ el.dispatchEvent(new MouseEvent('mousedown', { bubbles: true }));
523
+ el.dispatchEvent(new MouseEvent('mouseup', { bubbles: true }));
524
+ el.click();
525
+ };
526
+ const clearBtn = document.querySelector('.title-clear-all');
527
+ if (!clearBtn) return false;
528
+ fire(clearBtn);
529
+ await sleep(800);
530
+ const confirm = Array.from(document.querySelectorAll('button, [role="button"]'))
531
+ .find((b) => b.offsetParent !== null && (b.innerText || '').replace(/\\s+/g, '') === '重新上传');
532
+ if (confirm) fire(confirm);
533
+ return !!confirm;
534
+ })()
535
+ `);for(let Q=0;Q<10;Q++){await z.wait({time:0.5});K=await J();if(K?.cleanSurface&&!K?.restored)return!0}}await z.screenshot({path:"/tmp/xhs_publish_restore_debug.png"});throw new H("检测到小红书恢复了上次未发布的编辑内容(含旧图),自动清空失败——继续发布会导致图片重复上传。"+"请到 creator.xiaohongshu.com 发布页手动删除恢复的内容后重试。Debug: /tmp/xhs_publish_restore_debug.png")}Qz({site:"xiaohongshu",name:"publish",access:"write",description:"小红书发布图文笔记 (creator center UI automation)",domain:"creator.xiaohongshu.com",strategy:Zz.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:`文字配图卡片文字,多张卡片用 ${u} 分隔,卡内换行用 \\n`},{name:"card-style",required:!1,help:`文字配图卡片样式,运行时按页面实际选项匹配;找不到会失败。省略时使用${D}。可选: ${d.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 X("正文位置参数和 --file 只能二选一,不能同时给");const V=B.resolve(Z);if(!y.existsSync(V))throw new X(`--file 指向的文件不存在: ${V}`);Q=y.readFileSync(V,"utf-8").trim();if(!Q)throw new X(`--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"]):"",v=W?W.split(u).map((V)=>V.trim()).filter(Boolean):[],b=J["card-style"]?String(J["card-style"]).trim():"",U=v.length>0;if(!K)throw new X("--title is required");if(K.length>T)throw new X(`Title is ${K.length} chars — must be ≤ ${T}`);if(!Q)throw new X("缺少笔记正文:给位置参数 <content>(字面文本),或用 --file <本机文件路径>");$z(Q,{fileFlag:"--file"});if(!U&&q.length===0)throw new X("Provide --card-text (text-image mode) or --images (upload mode); neither was given.");if(q.length>C)throw new X(`Too many images: ${q.length} (max ${C})`);if(U&&q.some((V)=>B.extname(V).toLowerCase()===".gif"))throw new X("文字配图模式追加的图片不支持 .gif(编辑器图片入口只接受 jpg/jpeg/png/webp)");const F=Hz(q);await z.goto(qz);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 N=await Wz(z);if((await kz(z,N?.ok?5000:2000)).state==="video_surface"){await z.screenshot({path:"/tmp/xhs_publish_tab_debug.png"});const V=N?.ok?`clicked "${N.text}"`:`visible candidates: ${(N?.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`)}await Pz(z);let h=b;if(U)h=await hz(z,v,b);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:x/1000});await r(z);const O=await t(z);if(O&&typeof O.count==="number"&&O.count>F.length){await z.screenshot({path:"/tmp/xhs_publish_dup_debug.png"});throw new H(`上传后编辑器内有 ${O.count} 张图,超过本次注入的 ${F.length} 张——`+"疑似页面残留了上次未发布的旧图,已中止发布以避免图片重复。Debug: /tmp/xhs_publish_dup_debug.png")}}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(U)await a(z,v.length,"文字配图 generated images");if(U&&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:x/1000});await r(z);await a(z,v.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 Xz(z,m,$);const R=j?["暂存离开","存草稿"]:["发布","发布笔记"],G=await z.evaluate(`
510
536
  (cfg => {
511
537
  const { isDraftMode, publishNames, draftNames, labels } = cfg;
512
538
  const isVisible = (el) => {
@@ -545,7 +571,7 @@ import*as y from"node:fs";import*as U from"node:path";import{CommandExecutionErr
545
571
  }
546
572
  }
547
573
  return { ok: false, via: 'none', hosts: hosts.length, lastMethodError };
548
- })(${JSON.stringify({isDraftMode:j,publishNames:qz,draftNames:jz,labels:R})})
574
+ })(${JSON.stringify({isDraftMode:j,publishNames:jz,draftNames:Vz,labels:R})})
549
575
  `);if(!G?.ok){if(j){if(await z.evaluate(`
550
576
  (() => {
551
577
  const labels = ['返回', '关闭', '取消', '离开'];
@@ -559,7 +585,7 @@ import*as y from"node:fs";import*as U from"node:path";import{CommandExecutionErr
559
585
  }
560
586
  return false;
561
587
  })()
562
- `)){await z.wait({time:1});const k=["暂存离开","存草稿","保存草稿"];if(await z.evaluate(`
588
+ `)){await z.wait({time:1});const O=["暂存离开","存草稿","保存草稿"];if(await z.evaluate(`
563
589
  (labels => {
564
590
  const buttons = document.querySelectorAll('button, [role="button"]');
565
591
  for (const btn of buttons) {
@@ -574,7 +600,7 @@ import*as y from"node:fs";import*as U from"node:path";import{CommandExecutionErr
574
600
  }
575
601
  }
576
602
  return false;
577
- })(${JSON.stringify(k)})
603
+ })(${JSON.stringify(O)})
578
604
  `)){G.ok=!0;G.via="leave-save-fallback"}}if(!G?.ok){await z.wait({time:2});if(await z.evaluate(`
579
605
  () => {
580
606
  const markers = ['草稿箱(', '保存于', '编辑于'];
@@ -584,7 +610,7 @@ import*as y from"node:fs";import*as U from"node:path";import{CommandExecutionErr
584
610
  }
585
611
  return false;
586
612
  }
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(`
613
+ `)){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})`:"",O=G?.error?`, error=${G.error}`:"",S=G?.lastMethodError?`, lastMethodError=${G.lastMethodError}`:"";throw Error(`Could not trigger "${R[0]}" action${V}${O}${S}. Debug screenshot: /tmp/xhs_publish_submit_debug.png`)}await z.wait({time:4});const k=await z.evaluate("() => location.href"),zz=j?["草稿已保存","暂存成功","保存成功","保存于","图文笔记("]:["发布成功","上传成功"],_=await z.evaluate(`
588
614
  (markers => {
589
615
  for (const el of document.querySelectorAll('*')) {
590
616
  if (el.tagName === 'STYLE' || el.tagName === 'SCRIPT') continue;
@@ -593,5 +619,5 @@ import*as y from"node:fs";import*as U from"node:path";import{CommandExecutionErr
593
619
  if (el.children.length === 0 && markers.some(marker => text.includes(marker))) return text;
594
620
  }
595
621
  return '';
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(" · ")}]}});
622
+ })(${JSON.stringify(zz)})
623
+ `),Jz=!k.includes("/publish/publish"),Kz=_.length>0||Jz,A=j?"暂存成功":"发布成功";if(!Kz)throw new H(`${A} could not be verified: no success marker or post-submit navigation was observed. `+(k?`Current URL: ${k}`:"Current URL was empty."));return[{status:`✅ ${A}`,detail:[`"${K}"`,U?`${v.length}张文字配图${F.length?` + ${F.length}张图片`:""}${h&&h!==D?` (${h})`:""}`:`${F.length}张图片`,P.length?`话题: ${P.join(" ")}`:"",_||k||""].filter(Boolean).join(" · ")}]}});