publishport-opencli 1.8.5-pp.30 → 1.8.5-pp.31
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.
|
@@ -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
|
|
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,
|
package/clis/oschina/article.js
CHANGED
|
@@ -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:$};
|