publishport-opencli 1.8.5-pp.33 → 1.8.5-pp.34
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.
|
@@ -684,6 +684,8 @@ var PP = (function () {
|
|
|
684
684
|
// uploadFn: async (src) => ({ url, attrs? }) | url。用于装不进声明式 spec 的平台
|
|
685
685
|
// (如掘金 ImageX 多步 + AWS4 签名):平台自己实现「拿到 src → 返回新 URL」即可。
|
|
686
686
|
// 同样统一处理 HTML <img> + Markdown ![](),去重 / skip / 失败兜底。
|
|
687
|
+
// opts.allowRelative:允许 uploadFn 返回站内相对路径(如思否 /img/<id>——其
|
|
688
|
+
// cover 字段和自家编辑器正文都只用这种路径,完整 URL 反而被拒)。
|
|
687
689
|
async function processImagesWith(content, uploadFn, opts) {
|
|
688
690
|
opts = opts || {};
|
|
689
691
|
var skip = opts.skip || [];
|
|
@@ -706,7 +708,8 @@ var PP = (function () {
|
|
|
706
708
|
var res = await uploadFn(src);
|
|
707
709
|
var url = (res && typeof res === 'object') ? res.url : res;
|
|
708
710
|
if (!url) throw new Error('uploadFn returned no url');
|
|
709
|
-
|
|
711
|
+
var isRel = String(url).charAt(0) === '/' && String(url).charAt(1) !== '/';
|
|
712
|
+
if (!isHttpUrl(url) && !(opts.allowRelative && isRel)) throw new Error('uploadFn 返回的不是合法 URL: ' + String(url).slice(0, 150));
|
|
710
713
|
cache[src] = { url: url, attrs: (res && res.attrs) || null };
|
|
711
714
|
report.uploaded.push({ src: src.slice(0, 120), url: url });
|
|
712
715
|
} catch (e) {
|
|
@@ -11,12 +11,12 @@ const __publish = (`+Q+`);
|
|
|
11
11
|
`:"")+`const __content0 = I.content;
|
|
12
12
|
let content = I.content;
|
|
13
13
|
if (I.outputFormat === "html" && I.preprocessConfig) content = PP.preprocess(content, I.preprocessConfig);
|
|
14
|
-
`+(X?`const __t = await PP.processImagesWith(content, (src) => __upload(src, PP), { skip: I.imageSkip });
|
|
14
|
+
`+(X?`const __t = await PP.processImagesWith(content, (src) => __upload(src, PP), { skip: I.imageSkip, allowRelative: I.imageAllowRelative });
|
|
15
15
|
`:`const __t = await PP.transferImages(content, I.imageSpec, I.imageSkip);
|
|
16
16
|
`)+`content = __t.content;
|
|
17
17
|
if (I.publishParams && typeof I.publishParams.cover === "string" && I.publishParams.cover) {
|
|
18
18
|
var __cvWrap = '<img src="' + I.publishParams.cover + '">';
|
|
19
|
-
`+(X?` var __cv = await PP.processImagesWith(__cvWrap, (src) => __upload(src, PP), { skip: I.imageSkip });
|
|
19
|
+
`+(X?` var __cv = await PP.processImagesWith(__cvWrap, (src) => __upload(src, PP), { skip: I.imageSkip, allowRelative: I.imageAllowRelative });
|
|
20
20
|
`:` var __cv = await PP.transferImages(__cvWrap, I.imageSpec, I.imageSkip);
|
|
21
21
|
`)+` if (__cv.failed.length) {
|
|
22
22
|
`+` return { ok: false, stage: "cover", message: "封面图转存失败:" + __cv.failed[0].error, uploaded: __t.uploaded, failed: __t.failed.concat(__cv.failed) };
|
|
@@ -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", detail: __pub && __pub.detail, 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(K,Q){const{title:X,body:Z,format:G="auto",draftOnly:D=!1,profile:q,publishParams:k=null}=Q;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");C(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 V of[..._.missing,...z.missing]){if(T.has(V.src))continue;T.add(V.src);N.push(V)}let Y=k;if(Y&&typeof Y.cover==="string"&&Y.cover){const V=Y.cover.trim();if(/["<>\s]/.test(V))throw new O("封面图路径/URL 含非法字符(引号/尖括号/空白):"+V.slice(0,120));if(I(V))try{const{dataUri:$}=await E(V);Y={...Y,cover:$}}catch($){throw new O("封面图读取失败:"+V+"("+String($&&$.message||$)+")")}else Y={...Y,cover:V}}await gotoWritePage(K,q.home,q.originRe);const R={title:X,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),B=await K.evaluate(x);if(!B||B.ok===!1){const V=B?.stage||"publish",$=B?.status!=null?` (HTTP ${B.status})`:"",M=B?.detail!=null?`;接口原始响应:${typeof B.detail==="string"?B.detail:JSON.stringify(B.detail)}`:"";throw new O(`[${V}] ${B?.message||"发布失败"}${$}${M}`)}return{id:B.id,url:B.url,draft:B.draft,images:{uploaded:B.uploaded||[],failed:(B.failed||[]).concat(N)}}}export const __test__={selectContent,originReFromHome,buildPublishJs,publishArticle};
|
|
34
|
+
})()`}export async function publishArticle(K,Q){const{title:X,body:Z,format:G="auto",draftOnly:D=!1,profile:q,publishParams:k=null}=Q;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");C(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 V of[..._.missing,...z.missing]){if(T.has(V.src))continue;T.add(V.src);N.push(V)}let Y=k;if(Y&&typeof Y.cover==="string"&&Y.cover){const V=Y.cover.trim();if(/["<>\s]/.test(V))throw new O("封面图路径/URL 含非法字符(引号/尖括号/空白):"+V.slice(0,120));if(I(V))try{const{dataUri:$}=await E(V);Y={...Y,cover:$}}catch($){throw new O("封面图读取失败:"+V+"("+String($&&$.message||$)+")")}else Y={...Y,cover:V}}await gotoWritePage(K,q.home,q.originRe);const R={title:X,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||[],imageAllowRelative:q.image?.allowRelative===!0,publishParams:Y},j=typeof q.image?.uploadFn==="function"?q.image.uploadFn.toString():null,x=buildPublishJs(R,q.publish.toString(),j),B=await K.evaluate(x);if(!B||B.ok===!1){const V=B?.stage||"publish",$=B?.status!=null?` (HTTP ${B.status})`:"",M=B?.detail!=null?`;接口原始响应:${typeof B.detail==="string"?B.detail:JSON.stringify(B.detail)}`:"";throw new O(`[${V}] ${B?.message||"发布失败"}${$}${M}`)}return{id:B.id,url:B.url,draft:B.draft,images:{uploaded:B.uploaded||[],failed:(B.failed||[]).concat(N)}}}export const __test__={selectContent,originReFromHome,buildPublishJs,publishArticle};
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import{CliError as N,CommandExecutionError as P}from"@jackwener/opencli/errors";import{cli as v,Strategy as p}from"@jackwener/opencli/registry";import{publishArticle as f}from"../_shared/article/publish.js";import{readFile as I,stat as w}from"node:fs/promises";function u(W){if(!W.execute)throw new N("INVALID_INPUT","本命令需要 --execute 才会真正发布,请确认后加上 --execute。注意:本报错仅表示缺少 --execute,并未校验其余参数,不能当 dry-run 预览用")}async function h(W){const H=typeof W.text==="string"?W.text:void 0,V=typeof W.file==="string"?W.file:void 0;if(H&&V)throw new N("INVALID_INPUT","请只传 <text> 或 --file 之一,不能同时使用");let B=H??"";if(V){let A;try{A=await w(V)}catch{throw new N("INVALID_INPUT",`文件不存在:${V}`)}if(!A.isFile())throw new N("INVALID_INPUT",`路径必须是可读文本文件:${V}`);let M;try{M=await I(V)}catch{throw new N("INVALID_INPUT",`无法读取文件:${V}`)}try{B=new TextDecoder("utf-8",{fatal:!0}).decode(M)}catch{throw new N("INVALID_INPUT",`文件编码不是 UTF-8:${V}`)}}if(!B.trim())throw new N("INVALID_INPUT","正文不能为空");return B}function x(W,H,V,B,A={}){return[{status:"success",outcome:B,message:W,target_type:H,target:V,...A}]}export const segmentfaultProfile={home:"https://segmentfault.com/write",outputFormat:"markdown",image:{skip:["image-static.segmentfault.com","avatar-static.segmentfault.com"],uploadFn:async(W,H)=>{const B=await(await fetch("https://segmentfault.com/write",{credentials:"include"})).text();let A=null;const M=B.match(/serverData"\s*:\s*\{\s*"Token"\s*:\s*"([^"]+)"/);if(M)A=M[1];else{const U="window.g_initialProps = ",R=B.indexOf(U);if(R!==-1){const q=B.indexOf(`;
|
|
1
|
+
import{CliError as N,CommandExecutionError as P}from"@jackwener/opencli/errors";import{cli as v,Strategy as p}from"@jackwener/opencli/registry";import{publishArticle as f}from"../_shared/article/publish.js";import{readFile as I,stat as w}from"node:fs/promises";function u(W){if(!W.execute)throw new N("INVALID_INPUT","本命令需要 --execute 才会真正发布,请确认后加上 --execute。注意:本报错仅表示缺少 --execute,并未校验其余参数,不能当 dry-run 预览用")}async function h(W){const H=typeof W.text==="string"?W.text:void 0,V=typeof W.file==="string"?W.file:void 0;if(H&&V)throw new N("INVALID_INPUT","请只传 <text> 或 --file 之一,不能同时使用");let B=H??"";if(V){let A;try{A=await w(V)}catch{throw new N("INVALID_INPUT",`文件不存在:${V}`)}if(!A.isFile())throw new N("INVALID_INPUT",`路径必须是可读文本文件:${V}`);let M;try{M=await I(V)}catch{throw new N("INVALID_INPUT",`无法读取文件:${V}`)}try{B=new TextDecoder("utf-8",{fatal:!0}).decode(M)}catch{throw new N("INVALID_INPUT",`文件编码不是 UTF-8:${V}`)}}if(!B.trim())throw new N("INVALID_INPUT","正文不能为空");return B}function x(W,H,V,B,A={}){return[{status:"success",outcome:B,message:W,target_type:H,target:V,...A}]}export const segmentfaultProfile={home:"https://segmentfault.com/write",outputFormat:"markdown",image:{skip:["image-static.segmentfault.com","avatar-static.segmentfault.com"],allowRelative:!0,uploadFn:async(W,H)=>{const B=await(await fetch("https://segmentfault.com/write",{credentials:"include"})).text();let A=null;const M=B.match(/serverData"\s*:\s*\{\s*"Token"\s*:\s*"([^"]+)"/);if(M)A=M[1];else{const U="window.g_initialProps = ",R=B.indexOf(U);if(R!==-1){const q=B.indexOf(`;
|
|
2
2
|
</script>`,R);if(q!==-1)try{const L=JSON.parse(B.substring(R+U.length,q));A=L&&L.global&&L.global.sessionInfo&&L.global.sessionInfo.key}catch(L){}}}if(!A)throw Error("获取思否 session token 失败");const O=await fetch(W,{credentials:"omit"});if(!O.ok)throw Error("下载图片失败:"+O.status);const b=await O.blob(),E=new FormData;E.append("image",b);const F=await(await fetch("https://segmentfault.com/gateway/image",{method:"POST",credentials:"include",headers:{token:A},body:E})).text();if(F==="Unauthorized"||F.includes("禁言")||F.includes("锁定"))throw Error(F==="Unauthorized"?"未授权,请检查登录态":F);let X;try{X=JSON.parse(F)}catch(U){throw Error("图片上传失败:"+F)}let T=null;if(X&&X.url)T=X.url;else if(X&&X.result)T=X.result;else if(Array.isArray(X)){if(X[0]===1)throw Error(X[1]||"图片上传失败");T=X[1]||(X[2]?"https://image-static.segmentfault.com/"+X[2]:null)}if(!T)throw Error("图片上传失败,服务端未返回 URL");return{url:T}}},publish:async(W,H)=>{const V=W.params||{},A=await(await fetch("https://segmentfault.com/write",{credentials:"include"})).text();let M=null;const O=A.match(/serverData"\s*:\s*\{\s*"Token"\s*:\s*"([^"]+)"/);if(O)M=O[1];else{const Q="window.g_initialProps = ",z=A.indexOf(Q);if(z!==-1){const J=A.indexOf(`;
|
|
3
3
|
</script>`,z);if(J!==-1)try{const K=JSON.parse(A.substring(z+Q.length,J));M=K&&K.global&&K.global.sessionInfo&&K.global.sessionInfo.key}catch(K){}}}if(!M)return{ok:!1,stage:"token",status:0,message:"获取思否 session token 失败:请确认已登录;账号首次发文会被新手确认页(/howtowrite)拦住,需先在网页端进一次写作页点「我已知晓,继续撰写」"};async function b(Q){let z=null;try{const D=(await fetch("/",{method:"HEAD"})).headers.get("date");if(D){const S=new Date(D).getTime();if(!isNaN(S))z=Math.floor(S/1000)}}catch($){}const J=Object.keys(window).find(($)=>$.startsWith("webpackChunk"));if(!J)return{__sfErr:"页面未就绪:未找到 webpack 运行时,无法复用思否请求签名"};let K;try{window[J].push([[Symbol("pp_sf_"+Math.random())],{},($)=>{K=$}])}catch($){return{__sfErr:"注入 webpack 探针失败:"+String($)}}if(!K||!K.m)return{__sfErr:"无法取得 webpack require(思否前端结构可能已变)"};let Y=null;for(const $ of Object.keys(K.m)){let D="";try{D=String(K.m[$])}catch(S){continue}if(/function getUrl/.test(D)&&D.indexOf("/gateway")!==-1){Y=$;break}}if(!Y)return{__sfErr:"未定位到思否请求签名模块(getUrl/gateway),站点前端结构可能已变,需更新 adapter"};let j;try{j=K(Y).default}catch($){return{__sfErr:"加载思否请求模块失败:"+String($)}}if(typeof j!=="function")return{__sfErr:"思否请求模块导出异常(default 非构造器)"};let Z=null;const C=XMLHttpRequest.prototype.open;XMLHttpRequest.prototype.open=function($,D){try{if(String(D).indexOf(Q)!==-1)Z=this}catch(S){}return C.apply(this,arguments)};try{const $=new j({Token:M,userAgent:navigator.userAgent});if(z)$.setServerTime(z);await $.send(Q,{method:"GET"}).catch(function(){})}catch($){XMLHttpRequest.prototype.open=C;return{__sfErr:"签名请求执行失败:"+String($)}}XMLHttpRequest.prototype.open=C;if(!Z)return{__sfErr:"签名请求未发出(Request.send 未触发 XHR)"};return{status:Z.status,raw:String(Z.responseText||"")}}if(W.draftOnly){const Q={title:W.title,tags:[],text:W.content,object_id:"",type:"article"},z=await fetch("https://segmentfault.com/gateway/draft",{method:"POST",credentials:"include",headers:{"Content-Type":"application/json",token:M,accept:"*/*"},body:JSON.stringify(Q)}),J=await z.text();if(J==="Unauthorized"||J.includes("禁言")||J.includes("锁定"))return{ok:!1,stage:"publish",status:z.status,message:J==="Unauthorized"?"未授权":J};let K;try{K=JSON.parse(J)}catch(Y){return{ok:!1,stage:"publish",status:z.status,message:"解析响应失败:"+J.slice(0,200)}}if(Array.isArray(K)){if(K[0]===1)return{ok:!1,stage:"publish",status:z.status,message:K[1]||"发布失败"};const Y=K[1];if(Y&&Y.id)return{ok:!0,draft:!0,id:String(Y.id),url:"https://segmentfault.com/write?draftId="+Y.id}}if(!K||!K.id){const Y=K&&(K.message||K.msg||K.error||K.errMsg)||J.slice(0,200);return{ok:!1,stage:"publish",status:z.status,message:Y}}return{ok:!0,draft:!0,id:String(K.id),url:"https://segmentfault.com/write?draftId="+K.id}}const E={"Content-Type":"application/json",token:M,accept:"*/*"},_=Array.isArray(V.tags)?V.tags.map((Q)=>String(Q).trim()).filter(Boolean):[];if(_.length===0)return{ok:!1,stage:"tags",message:"思否发文必须至少指定 1 个标签(--tags),合法标签可用 `segmentfault tags <名称>` 校验"};const F=[];for(const Q of _){const z=await b("/tag/"+encodeURIComponent(Q));if(z&&z.__sfErr)return{ok:!1,stage:"tags",message:"标签「"+Q+"」校验失败:"+z.__sfErr};let J;try{J=JSON.parse(z.raw)}catch(Y){return{ok:!1,stage:"tags",status:z.status,message:"解析标签「"+Q+"」响应失败(HTTP "+z.status+"):"+String(z.raw).slice(0,200)}}if(z.status!==200||typeof J==="string")return{ok:!1,stage:"tags",status:z.status,message:"思否标签接口请求被拒(HTTP "+z.status+"):"+(typeof J==="string"?J:String(z.raw).slice(0,120))};const K=J&&J.tag;if(!K||K.id==null)return{ok:!1,stage:"tags",message:"思否上找不到标签「"+Q+"」,请用 `segmentfault tags <名称>` 确认存在再发布"};F.push(K.id)}const X=Array.isArray(V.channels)?V.channels.map((Q)=>String(Q).trim()).filter(Boolean):[],T=[];if(X.length>0){let Q=[];const z=await b("/channels");if(z&&z.__sfErr)return{ok:!1,stage:"channels",message:"获取思否频道列表失败:"+z.__sfErr};let J;try{J=JSON.parse(z.raw)}catch(K){return{ok:!1,stage:"channels",status:z.status,message:"解析频道列表失败(HTTP "+z.status+"):"+String(z.raw).slice(0,200)}}if(z.status!==200||typeof J==="string")return{ok:!1,stage:"channels",status:z.status,message:"思否频道接口请求被拒(HTTP "+z.status+"):"+(typeof J==="string"?J:String(z.raw).slice(0,120))};if(Array.isArray(J))Q=J;else if(J&&Array.isArray(J.data))Q=J.data;else if(J&&Array.isArray(J.rows))Q=J.rows;else if(J&&Array.isArray(J.channels))Q=J.channels;else Q=[];for(const K of X){const Y=Q.find((j)=>j&&(String(j.name||j.title||j.text)===K||String(j.slug)===K));if(!Y||!Y.slug){const j=Q.map((Z)=>Z&&(Z.name||Z.title||Z.text)||"").filter(Boolean).join(" / ");return{ok:!1,stage:"channels",message:"思否上找不到频道「"+K+"」,可选:"+(j||"(频道列表为空)")}}T.push(Y.slug)}}let U="";if(V.cover){const Q=String(V.cover);if(Q.indexOf("/img/")===0)U=Q;else{let z;try{const Z=await fetch(Q,{credentials:"omit"});if(!Z.ok)return{ok:!1,stage:"cover",status:Z.status,message:"封面图下载失败(HTTP "+Z.status+"):"+Q.slice(0,120)};z=await Z.blob()}catch(Z){return{ok:!1,stage:"cover",message:"封面图下载失败("+String(Z&&Z.message||Z)+")。思否封面只收自家图床 /img/ 路径;建议 --cover 直接传本机图片路径,由命令自动转存"}}const J=new FormData;J.append("image",z);const K=await fetch("https://segmentfault.com/gateway/image",{method:"POST",credentials:"include",headers:{token:M},body:J}),Y=await K.text();let j;try{j=JSON.parse(Y)}catch(Z){return{ok:!1,stage:"cover",status:K.status,message:"封面图上传思否图床失败:"+Y.slice(0,120)}}U=j&&j.url||"";if(!U)return{ok:!1,stage:"cover",status:K.status,message:"封面图上传后未拿到 /img/ 路径:"+Y.slice(0,120)}}}const R={title:W.title,text:W.content,tags:F,blog_id:0,type:1};if(T.length>0)R.channel=T;if(U)R.cover=U;const q=await fetch("https://segmentfault.com/gateway/article",{method:"POST",credentials:"include",headers:E,body:JSON.stringify(R)}),L=await q.text();if(L==="Unauthorized"||L.includes("禁言")||L.includes("锁定"))return{ok:!1,stage:"publish",status:q.status,message:L==="Unauthorized"?"未授权,请检查登录态":L};let G;try{G=JSON.parse(L)}catch(Q){return{ok:!1,stage:"publish",status:q.status,message:"解析发布响应失败:"+L.slice(0,200)}}if(G&&(G.isError||G.error||G.errno||G.scene_id)){let Q=G.message||G.msg||G.error||"";if(G.scene_id)Q=(Q?Q+";":"")+"思否要求人机验证(geetest),无法在自动化环境完成";return{ok:!1,stage:"publish",status:q.status,message:Q||"发布失败:"+L.slice(0,200)}}const y=G&&G.data&&G.data.id;if(y==null)return{ok:!1,stage:"publish",status:q.status,message:G&&(G.message||G.msg)||"发布响应缺少文章 id:"+L.slice(0,200)};return{ok:!0,draft:!1,id:String(y),url:"https://segmentfault.com/a/"+y}}};v({site:"segmentfault",name:"article",access:"write",description:"发布文章到思否(SegmentFault)。默认正式发布,加 --draft 仅存草稿。正文默认 Markdown;外链图片自动转存到思否图床。正式发布必须指定标签(--tags),合法标签用 `segmentfault tags <名称>` 校验、频道用 `segmentfault channels` 列举。",domain:"segmentfault.com",strategy:p.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:"tags",help:"标签名,逗号分隔(正式发布必填,至少 1 个)。合法标签用 `segmentfault tags <名称>` 校验;草稿可省略。"},{name:"cover",help:"封面图(选填):本机图片路径或图片 URL,自动转存到思否图床。"},{name:"channels",help:"频道名,逗号分隔(选填)。合法值用 `segmentfault channels` 列举。"},{name:"draft",type:"boolean",help:"仅保存草稿,不正式发布"},{name:"execute",type:"boolean",help:"实际执行发布。不加此参数时命令拒绝写操作。"}],columns:["status","outcome","message","target_type","target","created_target","created_url"],func:async(W,H)=>{if(!W)throw new P("思否发布需要浏览器会话");u(H);const V=String(H.title??"").trim();if(!V)throw new N("INVALID_INPUT","文章标题不能为空");const B=await h(H),A=Boolean(H.draft),M=(U)=>typeof U==="string"?U.split(",").map((R)=>R.trim()).filter(Boolean):[],O=M(H.tags),b=M(H.channels),E=typeof H.cover==="string"?H.cover.trim():"";if(!A&&O.length===0)throw new N("INVALID_INPUT","思否正式发布必须指定 --tags(至少 1 个标签);合法标签用 `segmentfault tags <名称>` 校验,或加 --draft 只存草稿");const _=await f(W,{title:V,body:B,format:H.html?"html":"markdown",draftOnly:A,profile:segmentfaultProfile,publishParams:{tags:O,channels:b,cover:E}}),F=_.images.uploaded.length|0,X=_.images.failed.length|0;let T=_.draft?"已保存到思否草稿箱(需在网页上手动发布)":"已正式发布到思否";if(F||X)T+=`;图片:${F} 张转存成功${X?`,${X} 张失败`:""}`;return x(T,"article","",_.draft?"draft":"created",{created_target:(_.draft?"draft:":"article:")+_.id,created_url:_.url})}});
|
package/dist/src/browser/page.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import{sendCommand as L,sendCommandFull as Y}from"./daemon-client.js";import{buildEvaluateExpression as $}from"./utils.js";import{saveBase64ToFile as N}from"../utils.js";import{generateStealthJs as j}from"./stealth.js";import{waitForDomStableJs as
|
|
2
|
-
${
|
|
1
|
+
import{sendCommand as L,sendCommandFull as Y}from"./daemon-client.js";import{buildEvaluateExpression as $}from"./utils.js";import{saveBase64ToFile as N}from"../utils.js";import{generateStealthJs as j,generateBeforeUnloadGuardJs as O}from"./stealth.js";import{waitForDomStableJs as U}from"./dom-helpers.js";import{BasePage as R}from"./base-page.js";import{classifyBrowserError as _}from"./errors.js";import{log as h}from"../logger.js";function J(A){const G=(A instanceof Error?A.message:String(A)).toLowerCase();return G.includes("unknown action")&&G.includes("network-capture")||G.includes("network capture")&&G.includes("not supported")}function z(A){const D=A instanceof Error?A.message:String(A);return D.includes("stale page identity")||/^Page not found:\s*\S+\s*$/.test(D)}export class Page extends R{session;contextId;windowMode;surface;siteSession;_idleTimeout;constructor(A,D,G,H,V="browser",Q){super();this.session=A;this.contextId=G;this.windowMode=H;this.surface=V;this.siteSession=Q;this._idleTimeout=D}_page;_networkCaptureUnsupported=!1;_networkCaptureWarned=!1;_sessionOpts(){return{session:this.session,surface:this.surface,...this.contextId&&{contextId:this.contextId},...this._idleTimeout!=null&&{idleTimeout:this._idleTimeout},...this.windowMode&&{windowMode:this.windowMode},...this.siteSession&&{siteSession:this.siteSession}}}_cmdOpts(){return{session:this.session,surface:this.surface,...this.contextId&&{contextId:this.contextId},...this._page!==void 0&&{page:this._page},...this._idleTimeout!=null&&{idleTimeout:this._idleTimeout},...this.windowMode&&{windowMode:this.windowMode},...this.siteSession&&{siteSession:this.siteSession}}}async goto(A,D){try{await L("exec",{code:O(),...this._cmdOpts()})}catch{}let G;try{G=await Y("navigate",{url:A,...this._cmdOpts()})}catch(H){if(!z(H)||this._page===void 0)throw H;this._page=void 0;G=await Y("navigate",{url:A,...this._cmdOpts()})}if(G.page)this._page=G.page;this._lastUrl=A;if(D?.waitUntil!=="none"){const H=D?.settleMs??1000,Q={code:`${j()};
|
|
2
|
+
${U(H,Math.min(500,H))}`,...this._cmdOpts()};try{await L("exec",Q)}catch(W){const K=_(W);if(K.kind!=="target-navigation")throw W;try{await new Promise((X)=>setTimeout(X,K.delayMs));await L("exec",Q)}catch(X){if(_(X).kind!=="target-navigation")throw X}}}else try{await L("exec",{code:j(),...this._cmdOpts()})}catch{}}getActivePage(){return this._page}setActivePage(A){this._page=A;this._lastUrl=null}_markUnsupportedNetworkCapture(){this._networkCaptureUnsupported=!0;if(this._networkCaptureWarned)return;this._networkCaptureWarned=!0;h.warn("Browser Bridge extension does not support network capture; continuing without it. Explore output may miss API endpoints until you reload or reinstall the extension.")}async evaluate(A,...D){const G=$(A,D);try{return await L("exec",{code:G,...this._cmdOpts()})}catch(H){const V=_(H);if(V.kind!=="target-navigation")throw H;await new Promise((Q)=>setTimeout(Q,V.delayMs));return L("exec",{code:G,...this._cmdOpts()})}}async getCookies(A={}){const D=await L("cookies",{...this._sessionOpts(),...A});return Array.isArray(D)?D:[]}async closeWindow(){try{await L("close-window",{...this._sessionOpts()})}catch{}finally{this._page=void 0;this._lastUrl=null;this._networkCaptureUnsupported=!1;this._networkCaptureWarned=!1}}async tabs(){const A=await L("tabs",{op:"list",...this._sessionOpts()});return Array.isArray(A)?A:[]}async newTab(A){const D=await Y("tabs",{op:"new",...A!==void 0&&{url:A},...this._sessionOpts()});this._lastUrl=null;return D.page}async closeTab(A){const D={op:"close",...this._sessionOpts()};if(typeof A==="number")D.index=A;else if(typeof A==="string")D.page=A;else if(this._page!==void 0)D.page=this._page;const G=await L("tabs",D),H=typeof G?.closed==="string"?G.closed:void 0;if(H&&H===this._page||!H&&(A===void 0||A===this._page)){this._page=void 0;this._lastUrl=null}}async selectTab(A){const D=await Y("tabs",{op:"select",...typeof A==="number"?{index:A}:{page:A},...this._sessionOpts()});if(D.page)this._page=D.page;this._lastUrl=null}async screenshot(A={}){const D=await L("screenshot",{...this._cmdOpts(),format:A.format,quality:A.quality,fullPage:A.fullPage,width:A.width,height:A.height});if(A.path)await N(D,A.path);return D}async startNetworkCapture(A=""){if(this._networkCaptureUnsupported)return!1;try{await L("network-capture-start",{pattern:A,...this._cmdOpts()});return!0}catch(D){if(!J(D))throw D;this._markUnsupportedNetworkCapture();return!1}}async readNetworkCapture(){if(this._networkCaptureUnsupported)return[];try{const A=await L("network-capture-read",{...this._cmdOpts()});return Array.isArray(A)?A:[]}catch(A){if(!J(A))throw A;this._markUnsupportedNetworkCapture();return[]}}async waitForDownload(A="",D=30000){return await L("wait-download",{pattern:A,timeoutMs:D,...this._cmdOpts()})}async setFileInput(A,D){if(!(await L("set-file-input",{files:A,selector:D,...this._cmdOpts()}))?.count)throw Error("setFileInput returned no count — command may not be supported by the extension")}async insertText(A){if(!(await L("insert-text",{text:A,...this._cmdOpts()}))?.inserted)throw Error("insertText returned no inserted flag — command may not be supported by the extension")}async frames(){const A=await L("frames",{...this._cmdOpts()});return Array.isArray(A)?A:[]}async evaluateInFrame(A,D){const G=$(A);return L("exec",{code:G,frameIndex:D,...this._cmdOpts()})}async cdp(A,D={}){return L("cdp",{cdpMethod:A,cdpParams:D,...this._cmdOpts()})}async handleJavaScriptDialog(A,D){await this.cdp("Page.handleJavaScriptDialog",{accept:A,...D!==void 0&&{promptText:D}})}async tryNativeClick(A,D){try{await this.nativeClick(A,D);return!0}catch{return!1}}async clickWithQuads(A){const D=JSON.stringify(A),G=`[data-opencli-ref="${A.replace(/"/g,"\\\"")}"]`;await this.evaluate(`
|
|
3
3
|
(() => {
|
|
4
4
|
const el = document.querySelector('[data-opencli-ref="' + ${D} + '"]');
|
|
5
5
|
if (el) el.scrollIntoView({ behavior: 'instant', block: 'center' });
|
|
@@ -8,4 +8,11 @@
|
|
|
8
8
|
*
|
|
9
9
|
* Inspired by puppeteer-extra-plugin-stealth.
|
|
10
10
|
*/
|
|
11
|
+
/**
|
|
12
|
+
* [pp-only] 单独的 beforeunload 中和脚本,供 page.goto() 在 navigate **之前**
|
|
13
|
+
* 对【当前 document】跑一次——堵住 stealth 覆盖不到的「离开当前页」盲区
|
|
14
|
+
* (stealth 是导航后注入的,兜不住离开旧页那一刻弹的框)。无 stealth guard,
|
|
15
|
+
* 每次都真正执行,幂等安全。
|
|
16
|
+
*/
|
|
17
|
+
export declare function generateBeforeUnloadGuardJs(): string;
|
|
11
18
|
export declare function generateStealthJs(): string;
|
|
@@ -1,4 +1,43 @@
|
|
|
1
|
-
let
|
|
1
|
+
let j;const k=`
|
|
2
|
+
(function () {
|
|
3
|
+
try {
|
|
4
|
+
var _ep = EventTarget.prototype;
|
|
5
|
+
var _nativeAdd = _ep.__buNativeAdd || _ep.addEventListener;
|
|
6
|
+
try {
|
|
7
|
+
_nativeAdd.call(window, 'beforeunload', function (e) {
|
|
8
|
+
try { e.stopImmediatePropagation(); } catch (x) {}
|
|
9
|
+
try { e.preventDefault(); } catch (x) {}
|
|
10
|
+
try { delete e.returnValue; } catch (x) {}
|
|
11
|
+
try { e.returnValue = undefined; } catch (x) {}
|
|
12
|
+
}, true);
|
|
13
|
+
} catch (e) {}
|
|
14
|
+
try { window.onbeforeunload = null; } catch (e) {}
|
|
15
|
+
try {
|
|
16
|
+
Object.defineProperty(window, 'onbeforeunload', { get: function () { return null; }, set: function () {}, configurable: true });
|
|
17
|
+
} catch (e) {}
|
|
18
|
+
if (!_ep.__buGuard) {
|
|
19
|
+
var _add = _ep.addEventListener;
|
|
20
|
+
var _rm = _ep.removeEventListener;
|
|
21
|
+
var addP = function (type, listener, options) {
|
|
22
|
+
if (type === 'beforeunload') return;
|
|
23
|
+
return _add.call(this, type, listener, options);
|
|
24
|
+
};
|
|
25
|
+
var rmP = function (type, listener, options) {
|
|
26
|
+
if (type === 'beforeunload') return;
|
|
27
|
+
return _rm.call(this, type, listener, options);
|
|
28
|
+
};
|
|
29
|
+
try { Object.defineProperty(addP, 'name', { value: 'addEventListener' }); } catch (e) {}
|
|
30
|
+
try { Object.defineProperty(rmP, 'name', { value: 'removeEventListener' }); } catch (e) {}
|
|
31
|
+
try { addP.toString = function () { return _add.toString(); }; } catch (e) {}
|
|
32
|
+
try { rmP.toString = function () { return _rm.toString(); }; } catch (e) {}
|
|
33
|
+
try { Object.defineProperty(_ep, '__buNativeAdd', { value: _add, enumerable: false, configurable: true }); } catch (e) {}
|
|
34
|
+
_ep.addEventListener = addP;
|
|
35
|
+
_ep.removeEventListener = rmP;
|
|
36
|
+
try { Object.defineProperty(_ep, '__buGuard', { value: true, enumerable: false, configurable: true }); } catch (e) {}
|
|
37
|
+
}
|
|
38
|
+
} catch (e) {}
|
|
39
|
+
})();
|
|
40
|
+
`;export function generateBeforeUnloadGuardJs(){return k}export function generateStealthJs(){if(j!==void 0)return j;return j=`
|
|
2
41
|
(() => {
|
|
3
42
|
// Guard: prevent double-injection across separate CDP evaluations.
|
|
4
43
|
// We cannot use a closure variable (each eval is a fresh scope), and
|
|
@@ -341,26 +380,13 @@ let e;export function generateStealthJs(){if(e!==void 0)return e;return e=`
|
|
|
341
380
|
// navigate/exec aborts with "This operation was aborted" until a
|
|
342
381
|
// human manually clicks "Stay". This repeatedly broke twitter
|
|
343
382
|
// post/reply on the image->text transition and poisoned later reads.
|
|
344
|
-
// Defense:
|
|
345
|
-
//
|
|
346
|
-
//
|
|
347
|
-
// on every navigation
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
try { e.preventDefault(); } catch {}
|
|
352
|
-
try { delete e.returnValue; } catch {}
|
|
353
|
-
try { e.returnValue = undefined; } catch {}
|
|
354
|
-
}, true);
|
|
355
|
-
try { window.onbeforeunload = null; } catch {}
|
|
356
|
-
try {
|
|
357
|
-
Object.defineProperty(window, 'onbeforeunload', {
|
|
358
|
-
get: () => null,
|
|
359
|
-
set: () => {},
|
|
360
|
-
configurable: true,
|
|
361
|
-
});
|
|
362
|
-
} catch {}
|
|
363
|
-
} catch {}
|
|
383
|
+
// Defense (shared BEFOREUNLOAD_NEUTRALIZE): swallow already-armed
|
|
384
|
+
// handlers in the capture phase, drop future addEventListener
|
|
385
|
+
// registrations at the source, and no-op the onbeforeunload setter —
|
|
386
|
+
// so no dialog can ever open. Injected on every navigation, covering
|
|
387
|
+
// post, reply, and reads uniformly; page.goto() also runs the same
|
|
388
|
+
// snippet before navigating away to cover the current document.
|
|
389
|
+
${k}
|
|
364
390
|
|
|
365
391
|
return 'applied';
|
|
366
392
|
})()
|