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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/cli-manifest.json +20 -8
  2. package/clis/_shared/article/publish.js +3 -3
  3. package/clis/_shared/content-guard.js +1 -0
  4. package/clis/bilibili/comment.js +1 -1
  5. package/clis/bilibili/dynamic-post.js +1 -1
  6. package/clis/devto/publish.js +2 -2
  7. package/clis/hashnode/publish.js +8 -8
  8. package/clis/juejin/stats.js +1 -1
  9. package/clis/nowcoder/comment.js +1 -1
  10. package/clis/nowcoder/dm-send.js +1 -1
  11. package/clis/nowcoder/post.js +1 -1
  12. package/clis/qiita/publish.js +4 -4
  13. package/clis/segmentfault/_signed-request.js +43 -0
  14. package/clis/segmentfault/article.js +3 -3
  15. package/clis/segmentfault/channels.js +1 -1
  16. package/clis/segmentfault/tags.js +1 -1
  17. package/clis/tumblr/publish.js +2 -2
  18. package/clis/twitter/dm-send.js +4 -4
  19. package/clis/twitter/post.js +11 -11
  20. package/clis/twitter/reply-dm.js +3 -3
  21. package/clis/twitter/reply.js +11 -11
  22. package/clis/weibo/auth.js +3 -3
  23. package/clis/xiaohongshu/publish.js +32 -32
  24. package/clis/zhihu/answer.js +1 -1
  25. package/dist/src/browser/cdp.d.ts +5 -0
  26. package/dist/src/browser/cdp.js +1 -1
  27. package/dist/src/browser/fingerprint.d.ts +44 -0
  28. package/dist/src/browser/fingerprint.js +82 -0
  29. package/dist/src/browser/managed-chrome.d.ts +61 -3
  30. package/dist/src/browser/managed-chrome.js +1 -1
  31. package/dist/src/browser/profile.d.ts +49 -0
  32. package/dist/src/browser/profile.js +1 -1
  33. package/dist/src/browser/proxy-test.d.ts +28 -0
  34. package/dist/src/browser/proxy-test.js +12 -0
  35. package/dist/src/cli.js +22 -22
  36. package/dist/src/execution.js +2 -2
  37. package/dist/src/launcher.d.ts +1 -1
  38. package/dist/src/launcher.js +5 -5
  39. package/dist/src/runtime.d.ts +10 -0
  40. package/dist/src/runtime.js +1 -1
  41. package/package.json +1 -1
package/cli-manifest.json CHANGED
@@ -20704,7 +20704,7 @@
20704
20704
  {
20705
20705
  "site": "juejin",
20706
20706
  "name": "stats",
20707
- "description": "掘金文章数据(每篇展现/阅读/点赞/评论/收藏/阅读比/热度等全量运营指标)",
20707
+ "description": "掘金文章数据(每篇展现/阅读/点赞/评论/收藏/阅读比/热度等全量运营指标)。首次调用要拉起浏览器会话、偏慢,客户端超时属正常,重试一次即可",
20708
20708
  "access": "read",
20709
20709
  "domain": "juejin.cn",
20710
20710
  "strategy": "cookie",
@@ -32669,14 +32669,14 @@
32669
32669
  {
32670
32670
  "site": "segmentfault",
32671
32671
  "name": "channels",
32672
- "description": "列出思否文章频道(id + 名称),供 `segmentfault article --channels` 取合法频道名。",
32672
+ "description": "列出思否文章频道(名称 + slug),供 `segmentfault article --channels` 取合法频道名。",
32673
32673
  "access": "read",
32674
32674
  "domain": "segmentfault.com",
32675
32675
  "strategy": "cookie",
32676
32676
  "browser": true,
32677
32677
  "args": [],
32678
32678
  "columns": [
32679
- "channel_id",
32679
+ "channel_slug",
32680
32680
  "channel_name"
32681
32681
  ],
32682
32682
  "type": "js",
@@ -40488,9 +40488,15 @@
40488
40488
  {
40489
40489
  "name": "text",
40490
40490
  "type": "string",
40491
- "required": true,
40491
+ "required": false,
40492
40492
  "positional": true,
40493
- "help": "The text content of your reply"
40493
+ "help": "The text content of your reply (or use --file)"
40494
+ },
40495
+ {
40496
+ "name": "file",
40497
+ "type": "str",
40498
+ "required": false,
40499
+ "help": "从本机文件读取回复正文(UTF-8),与位置参数 <text> 二选一"
40494
40500
  },
40495
40501
  {
40496
40502
  "name": "image",
@@ -45507,9 +45513,15 @@
45507
45513
  {
45508
45514
  "name": "content",
45509
45515
  "type": "str",
45510
- "required": true,
45516
+ "required": false,
45511
45517
  "positional": true,
45512
- "help": "笔记正文"
45518
+ "help": "笔记正文(字面文本,不会展开 @文件 引用;长正文建议用 --file)"
45519
+ },
45520
+ {
45521
+ "name": "file",
45522
+ "type": "str",
45523
+ "required": false,
45524
+ "help": "从本机文件读取笔记正文(UTF-8),与位置参数 <content> 二选一"
45513
45525
  },
45514
45526
  {
45515
45527
  "name": "title",
@@ -48170,7 +48182,7 @@
48170
48182
  "type": "str",
48171
48183
  "required": true,
48172
48184
  "positional": true,
48173
- "help": "Zhihu question URL or typed target"
48185
+ "help": "Zhihu question URL (e.g. https://www.zhihu.com/question/461491497) or typed target question:461491497; bare numeric id is rejected"
48174
48186
  },
48175
48187
  {
48176
48188
  "name": "text",
@@ -1,10 +1,10 @@
1
- import{CommandExecutionError as O}from"@jackwener/opencli/errors";import{normalizeContent as H}from"./format.js";import{inlineLocalImages as L,isLocalImagePath as C,localImageToDataUri as M}from"./images.js";import{PAGE_RUNTIME as I}from"./page-runtime.js";export function selectContent(B,K,V="auto"){const Z=H(B,{format:V});return K.outputFormat==="markdown"?Z.markdown:Z.html}export function originReFromHome(B){let K="";try{K=new URL(B).host}catch(Z){K=""}return"^https?://([^/]*\\.)?"+K.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")+"(/|$)"}export async function gotoWritePage(B,K,V){const Z=V||originReFromHome(K);for(let G=0;G<8;G++){await B.goto(K);await B.wait({time:1});const D=await B.evaluate(`(() => {
1
+ import{CommandExecutionError as O}from"@jackwener/opencli/errors";import{normalizeContent as H}from"./format.js";import{assertLiteralContent as M}from"../content-guard.js";import{inlineLocalImages as U,isLocalImagePath as C,localImageToDataUri as I}from"./images.js";import{PAGE_RUNTIME as E}from"./page-runtime.js";export function selectContent(B,K,V="auto"){const Z=H(B,{format:V});return K.outputFormat==="markdown"?Z.markdown:Z.html}export function originReFromHome(B){let K="";try{K=new URL(B).host}catch(Z){K=""}return"^https?://([^/]*\\.)?"+K.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")+"(/|$)"}export async function gotoWritePage(B,K,V){const Z=V||originReFromHome(K);for(let G=0;G<8;G++){await B.goto(K);await B.wait({time:1});const D=await B.evaluate(`(() => {
2
2
  var href = ''; try { href = location.href; } catch (e) {}
3
3
  var onSite = new RegExp(${JSON.stringify(Z)}).test(href);
4
4
  var cookieOk = true; try { void document.cookie; } catch (e) { cookieOk = false; }
5
5
  return { href: href, onSite: onSite, cookieOk: cookieOk };
6
6
  })()`);if(D&&D.onSite&&D.cookieOk)return;await B.wait({time:1})}throw new O(`无法在浏览器中打开已登录的写作页(页面停留在空白页,未落到 ${K})。`+"请确认登录该平台的 Chrome 已连接 opencli 浏览器桥。")}export function buildPublishJs(B,K,V){return`(async () => {
7
- `+I+`
7
+ `+E+`
8
8
  const I = `+JSON.stringify(B)+`;
9
9
  const __publish = (`+K+`);
10
10
  `+(V?"const __upload = ("+V+`);
@@ -31,4 +31,4 @@ if (!__pub || __pub.ok === false) {
31
31
  return { ok: false, stage: (__pub && __pub.stage) || "publish", status: __pub && __pub.status, message: (__pub && __pub.message) || "publish failed", uploaded: __upN, failed: __failN };
32
32
  }
33
33
  return { ok: true, id: String(__pub.id == null ? "" : __pub.id), url: __pub.url || "", draft: !!__pub.draft, uploaded: __upN, failed: __failN };
34
- })()`}export async function publishArticle(B,K){const{title:V,body:Z,format:G="auto",draftOnly:D=!1,profile:q,publishParams:U=null}=K;if(!q)throw Error("publishArticle: profile is required");if(!q.home)throw Error("publishArticle: profile.home is required");if(typeof q.publish!=="function")throw Error("publishArticle: profile.publish must be a function");const W=H(Z,{format:G}),k=q.outputFormat==="markdown"||q.needsMarkdown===!0,w=q.outputFormat==="html"||q.needsHtml===!0,_=k?await L(W.markdown):{content:null,missing:[]},z=w?await L(W.html):{content:null,missing:[]},A=_.content,J=z.content,y=q.outputFormat==="markdown"?A:J,N=[],T=new Set;for(const Q of[..._.missing,...z.missing]){if(T.has(Q.src))continue;T.add(Q.src);N.push(Q)}let Y=U;if(Y&&typeof Y.cover==="string"&&Y.cover){const Q=Y.cover.trim();if(/["<>\s]/.test(Q))throw new O("封面图路径/URL 含非法字符(引号/尖括号/空白):"+Q.slice(0,120));if(C(Q))try{const{dataUri:$}=await M(Q);Y={...Y,cover:$}}catch($){throw new O("封面图读取失败:"+Q+"("+String($&&$.message||$)+")")}else Y={...Y,cover:Q}}await gotoWritePage(B,q.home,q.originRe);const j={title:V,content:y,markdown:q.needsMarkdown===!0&&q.outputFormat!=="markdown"?A:null,html:q.needsHtml===!0&&q.outputFormat!=="html"?J:null,draftOnly:!!D,outputFormat:q.outputFormat,preprocessConfig:q.preprocessConfig||null,imageSpec:q.image?.spec||null,imageSkip:q.image?.skip||[],publishParams:Y},R=typeof q.image?.uploadFn==="function"?q.image.uploadFn.toString():null,x=buildPublishJs(j,q.publish.toString(),R),X=await B.evaluate(x);if(!X||X.ok===!1){const Q=X?.stage||"publish",$=X?.status!=null?` (HTTP ${X.status})`:"";throw new O(`[${Q}] ${X?.message||"发布失败"}${$}`)}return{id:X.id,url:X.url,draft:X.draft,images:{uploaded:X.uploaded||[],failed:(X.failed||[]).concat(N)}}}export const __test__={selectContent,originReFromHome,buildPublishJs,publishArticle};
34
+ })()`}export async function publishArticle(B,K){const{title:V,body:Z,format:G="auto",draftOnly:D=!1,profile:q,publishParams:k=null}=K;if(!q)throw Error("publishArticle: profile is required");if(!q.home)throw Error("publishArticle: profile.home is required");if(typeof q.publish!=="function")throw Error("publishArticle: profile.publish must be a function");M(Z,{fileFlag:"--file"});const W=H(Z,{format:G}),w=q.outputFormat==="markdown"||q.needsMarkdown===!0,L=q.outputFormat==="html"||q.needsHtml===!0,_=w?await U(W.markdown):{content:null,missing:[]},z=L?await U(W.html):{content:null,missing:[]},A=_.content,J=z.content,y=q.outputFormat==="markdown"?A:J,N=[],T=new Set;for(const Q of[..._.missing,...z.missing]){if(T.has(Q.src))continue;T.add(Q.src);N.push(Q)}let Y=k;if(Y&&typeof Y.cover==="string"&&Y.cover){const Q=Y.cover.trim();if(/["<>\s]/.test(Q))throw new O("封面图路径/URL 含非法字符(引号/尖括号/空白):"+Q.slice(0,120));if(C(Q))try{const{dataUri:$}=await I(Q);Y={...Y,cover:$}}catch($){throw new O("封面图读取失败:"+Q+"("+String($&&$.message||$)+")")}else Y={...Y,cover:Q}}await gotoWritePage(B,q.home,q.originRe);const R={title:V,content:y,markdown:q.needsMarkdown===!0&&q.outputFormat!=="markdown"?A:null,html:q.needsHtml===!0&&q.outputFormat!=="html"?J:null,draftOnly:!!D,outputFormat:q.outputFormat,preprocessConfig:q.preprocessConfig||null,imageSpec:q.image?.spec||null,imageSkip:q.image?.skip||[],publishParams:Y},j=typeof q.image?.uploadFn==="function"?q.image.uploadFn.toString():null,x=buildPublishJs(R,q.publish.toString(),j),X=await B.evaluate(x);if(!X||X.ok===!1){const Q=X?.stage||"publish",$=X?.status!=null?` (HTTP ${X.status})`:"";throw new O(`[${Q}] ${X?.message||"发布失败"}${$}`)}return{id:X.id,url:X.url,draft:X.draft,images:{uploaded:X.uploaded||[],failed:(X.failed||[]).concat(N)}}}export const __test__={selectContent,originReFromHome,buildPublishJs,publishArticle};
@@ -0,0 +1 @@
1
+ import*as J from"node:fs";import*as K from"node:os";import{ArgumentError as G}from"@jackwener/opencli/errors";const M=/^@(?:\/|~\/|\.{1,2}\/)\S+$/,N=/^(?:\/|~\/)\S+$/;function O(y){return y.startsWith("~/")?`${K.homedir()}/${y.slice(2)}`:y}export function assertLiteralContent(y,{fileFlag:C=null,label:q="正文"}={}){const h=String(y??"").trim();if(!h)return h;const D=C?(z)=>`请改用 ${C} ${z},或把文件内容读出来作为${q}传入`:()=>`本命令没有文件参数,请把文件内容读出来作为${q}传入`;if(M.test(h))throw new G(`${q} "${h}" 看起来是文件引用——命令不会展开 @路径,照发会把这串字面量当${q}。${D(h.slice(1))}`);if(N.test(h)){let z=!1;try{z=J.statSync(O(h)).isFile()}catch{}if(z)throw new G(`${q} "${h}" 是一个本机已存在的文件路径——照发会把这串路径当${q}发出去。${D(h)}`)}return h}
@@ -1 +1 @@
1
- import{cli as B,Strategy as G}from"@jackwener/opencli/registry";import{ArgumentError as Q,CommandExecutionError as V,EmptyResultError as I}from"@jackwener/opencli/errors";import{apiGet as S,apiPost as U,requireOkPayload as Z,resolveBvid as q,resolveUid as O}from"./utils.js";function R(D,f){const z=Number(D);if(!Number.isInteger(z)||z<=0)throw new Q(`bilibili comment ${f} must be a positive integer`);return z}B({site:"bilibili",name:"comment",access:"write",description:"在 B站视频下发表评论或回复(官方 API,需登录;消息里的 @用户 会被解析为真实提及)",domain:"www.bilibili.com",strategy:G.COOKIE,args:[{name:"bvid",required:!0,positional:!0,help:"Video BV ID / URL / b23.tv short link"},{name:"message",required:!0,positional:!0,help:"Comment text. Any @username in it is resolved to a real mention"},{name:"parent",type:"int",help:"top-level/root rpid to reply under (omit for a top-level comment)"},{name:"execute",type:"boolean",help:"Actually post the comment. Without it the command refuses to write."}],columns:["rpid","bvid","oid","message","url"],func:async(D,f)=>{if(!D)throw new V("Browser session required for bilibili comment");const z=String(f.message??"").trim();if(!z)throw new Q("bilibili comment message cannot be empty");if(!f.execute)throw new Q("Refusing to post: pass --execute to actually publish this comment");const W=f.parent!=null?R(f.parent,"parent"):null;let H;try{H=await q(f.bvid)}catch(J){throw new Q(`Cannot resolve Bilibili BV ID from input: ${String(f.bvid??"")}`,J instanceof Error?J.message:String(J))}const _=await S(D,"/x/web-interface/view",{params:{bvid:H}}),X=Z(_,"view")?.aid;if(!X)throw new V(`Cannot resolve aid for bvid: ${H}`);const K={};for(const J of z.matchAll(/@([^\s@]+)/g)){const L=J[1];if(L in K)continue;try{const F=Number(await O(D,L));if(!Number.isInteger(F)||F<=0)throw new V(`Bilibili user search returned malformed mid for @${L}`);K[L]=F}catch(F){if(!(F instanceof I))throw F}}const $={oid:X,type:1,message:z,plat:1,...W!=null?{root:W,parent:W}:{},...Object.keys(K).length>0?{at_name_to_mid:JSON.stringify(K)}:{}},h=await U(D,"/x/v2/reply/add",{params:$}),Y=Z(h,"reply add")?.rpid;if(!Y)throw new V("Bilibili reply add API did not return rpid for the posted comment");return[{rpid:String(Y),bvid:H,oid:String(X),message:z,url:`https://www.bilibili.com/video/${H}#reply${Y}`}]}});
1
+ import{cli as L,Strategy as S}from"@jackwener/opencli/registry";import{ArgumentError as V,CommandExecutionError as W,EmptyResultError as U}from"@jackwener/opencli/errors";import{apiGet as _,apiPost as h,requireOkPayload as $,resolveBvid as q,resolveUid as O}from"./utils.js";import{assertLiteralContent as R}from"../_shared/content-guard.js";function j(D,z){const f=Number(D);if(!Number.isInteger(f)||f<=0)throw new V(`bilibili comment ${z} must be a positive integer`);return f}L({site:"bilibili",name:"comment",access:"write",description:"在 B站视频下发表评论或回复(官方 API,需登录;消息里的 @用户 会被解析为真实提及)",domain:"www.bilibili.com",strategy:S.COOKIE,args:[{name:"bvid",required:!0,positional:!0,help:"Video BV ID / URL / b23.tv short link"},{name:"message",required:!0,positional:!0,help:"Comment text. Any @username in it is resolved to a real mention"},{name:"parent",type:"int",help:"top-level/root rpid to reply under (omit for a top-level comment)"},{name:"execute",type:"boolean",help:"Actually post the comment. Without it the command refuses to write."}],columns:["rpid","bvid","oid","message","url"],func:async(D,z)=>{if(!D)throw new W("Browser session required for bilibili comment");const f=String(z.message??"").trim();R(f,{label:"评论内容"});if(!f)throw new V("bilibili comment message cannot be empty");if(!z.execute)throw new V("Refusing to post: pass --execute to actually publish this comment");const X=z.parent!=null?j(z.parent,"parent"):null;let H;try{H=await q(z.bvid)}catch(J){throw new V(`Cannot resolve Bilibili BV ID from input: ${String(z.bvid??"")}`,J instanceof Error?J.message:String(J))}const B=await _(D,"/x/web-interface/view",{params:{bvid:H}}),Y=$(B,"view")?.aid;if(!Y)throw new W(`Cannot resolve aid for bvid: ${H}`);const K={};for(const J of f.matchAll(/@([^\s@]+)/g)){const Q=J[1];if(Q in K)continue;try{const F=Number(await O(D,Q));if(!Number.isInteger(F)||F<=0)throw new W(`Bilibili user search returned malformed mid for @${Q}`);K[Q]=F}catch(F){if(!(F instanceof U))throw F}}const G={oid:Y,type:1,message:f,plat:1,...X!=null?{root:X,parent:X}:{},...Object.keys(K).length>0?{at_name_to_mid:JSON.stringify(K)}:{}},I=await h(D,"/x/v2/reply/add",{params:G}),Z=$(I,"reply add")?.rpid;if(!Z)throw new W("Bilibili reply add API did not return rpid for the posted comment");return[{rpid:String(Z),bvid:H,oid:String(Y),message:f,url:`https://www.bilibili.com/video/${H}#reply${Z}`}]}});
@@ -1,4 +1,4 @@
1
- import{cli as K,Strategy as M}from"@jackwener/opencli/registry";import{ArgumentError as J,CommandExecutionError as B}from"@jackwener/opencli/errors";K({site:"bilibili",name:"dynamic-post",access:"write",description:"发布 B站动态(纯文本,官方 API,需登录)",domain:"www.bilibili.com",strategy:M.COOKIE,browser:!0,args:[{name:"text",required:!0,positional:!0,help:"Dynamic text content"},{name:"execute",type:"boolean",help:"Actually publish. Without it the command refuses to write."}],columns:["status","dynamic_id","text","url"],func:async(q,D)=>{if(!q)throw new B("Browser session required for bilibili dynamic-post");const z=String(D.text??"").trim();if(!z)throw new J("bilibili dynamic-post text cannot be empty");if(!D.execute)throw new J("Refusing to post: pass --execute to actually publish this dynamic");let G=!1;for(let H=0;H<4;H++){await q.goto("https://t.bilibili.com");await q.wait(2);const I=await q.evaluate("location.href");if(typeof I==="string"&&/^https?:\/\/[^/]*bilibili\.com/.test(I)){G=!0;break}}if(!G)throw new B("Failed to navigate to t.bilibili.com (page did not land on a bilibili origin)");const v=await q.evaluate(`(async () => {
1
+ import{cli as K,Strategy as M}from"@jackwener/opencli/registry";import{ArgumentError as J,CommandExecutionError as B}from"@jackwener/opencli/errors";import{assertLiteralContent as N}from"../_shared/content-guard.js";K({site:"bilibili",name:"dynamic-post",access:"write",description:"发布 B站动态(纯文本,官方 API,需登录)",domain:"www.bilibili.com",strategy:M.COOKIE,browser:!0,args:[{name:"text",required:!0,positional:!0,help:"Dynamic text content"},{name:"execute",type:"boolean",help:"Actually publish. Without it the command refuses to write."}],columns:["status","dynamic_id","text","url"],func:async(q,D)=>{if(!q)throw new B("Browser session required for bilibili dynamic-post");const z=String(D.text??"").trim();N(z,{label:"动态内容"});if(!z)throw new J("bilibili dynamic-post text cannot be empty");if(!D.execute)throw new J("Refusing to post: pass --execute to actually publish this dynamic");let G=!1;for(let H=0;H<4;H++){await q.goto("https://t.bilibili.com");await q.wait(2);const I=await q.evaluate("location.href");if(typeof I==="string"&&/^https?:\/\/[^/]*bilibili\.com/.test(I)){G=!0;break}}if(!G)throw new B("Failed to navigate to t.bilibili.com (page did not land on a bilibili origin)");const v=await q.evaluate(`(async () => {
2
2
  var text = ${JSON.stringify(z)};
3
3
  var csrf = (document.cookie.match(/bili_jct=([^;]+)/) || [])[1] || '';
4
4
  var url = 'https://api.bilibili.com/x/dynamic/feed/create/dyn?csrf=' + encodeURIComponent(csrf);
@@ -1,4 +1,4 @@
1
- import*as V from"node:fs";import{cli as q,Strategy as B}from"@jackwener/opencli/registry";import{ArgumentError as K,AuthRequiredError as U,CommandExecutionError as O}from"@jackwener/opencli/errors";const W=4;export function resolveBody(z){if(z["body-file"]){const D=String(z["body-file"]);if(!V.statSync(D,{throwIfNoEntry:!1})?.isFile())throw new K(`--body-file not found: ${D}`);return V.readFileSync(D,"utf8")}if(z.body!==void 0&&z.body!==null)return String(z.body);return}export function resolveTagList(z){if(z===void 0||z===null||z==="")return;const D=String(z).split(",").map((F)=>F.trim().toLowerCase()).filter(Boolean);if(D.length>W)throw new K(`Too many tags: ${D.length} (DEV.to max ${W})`);for(const F of D)if(!/^[a-z0-9]+$/.test(F))throw new K(`Invalid tag "${F}". DEV.to tags must be lowercase alphanumeric (no spaces/punctuation).`);return D.join(", ")}export function buildArticle(z,D,F){const H={};if(z.title!==void 0)H.title=String(z.title);if(D!==void 0)H.body_markdown=D;if(z.published!==void 0)H.published=!!z.published;if(F!==void 0)H.tag_list=F;if(z["cover-image"])H.main_image=String(z["cover-image"]);if(z["canonical-url"])H.canonical_url=String(z["canonical-url"]);if(z.series)H.series=String(z.series);if(z.description)H.description=String(z.description);return H}q({site:"devto",name:"publish",access:"write",description:"Publish or update a DEV.to article (markdown). Draft by default; pass --published to go live.",domain:"dev.to",strategy:B.COOKIE,browser:!0,args:[{name:"title",type:"string",required:!1,help:"Article title (required for new articles)"},{name:"body",type:"string",required:!1,help:"Article body in Markdown (or use --body-file)"},{name:"body-file",type:"string",required:!1,help:"Path to a Markdown file for the body"},{name:"published",type:"boolean",required:!1,default:!1,help:"Publish live now (default: save as draft)"},{name:"tags",type:"string",required:!1,help:"Comma-separated tags, max 4, lowercase alphanumeric"},{name:"cover-image",type:"string",required:!1,help:"Cover image URL (main_image)"},{name:"canonical-url",type:"string",required:!1,help:"Canonical URL (original source)"},{name:"series",type:"string",required:!1,help:"Series name to group articles"},{name:"description",type:"string",required:!1,help:"Social/SEO description"},{name:"id",type:"string",required:!1,help:"Existing article id to update (PUT instead of POST)"}],columns:["status","id","url","published","slug"],func:async(z,D)=>{if(!z)throw new O("Browser session required for devto publish");const F=D.id?String(D.id).trim():"",H=resolveBody(D),Y=resolveTagList(D.tags);if(!F&&!D.title)throw new K("--title is required when creating a new article");if(!F&&H===void 0)throw new K("--body or --body-file is required when creating a new article");const P=buildArticle(D,H,Y),Z=F?"PUT":"POST",$=F?`/articles/${encodeURIComponent(F)}`:"/articles";await z.goto("https://dev.to/");const I=await z.evaluate(`(async () => {
1
+ import*as V from"node:fs";import{cli as q,Strategy as B}from"@jackwener/opencli/registry";import{ArgumentError as K,AuthRequiredError as U,CommandExecutionError as O}from"@jackwener/opencli/errors";import{assertLiteralContent as v}from"../_shared/content-guard.js";const W=4;export function resolveBody(z){if(z["body-file"]){const D=String(z["body-file"]);if(!V.statSync(D,{throwIfNoEntry:!1})?.isFile())throw new K(`--body-file not found: ${D}`);return V.readFileSync(D,"utf8")}if(z.body!==void 0&&z.body!==null){v(String(z.body),{fileFlag:"--body-file"});return String(z.body)}return}export function resolveTagList(z){if(z===void 0||z===null||z==="")return;const D=String(z).split(",").map((F)=>F.trim().toLowerCase()).filter(Boolean);if(D.length>W)throw new K(`Too many tags: ${D.length} (DEV.to max ${W})`);for(const F of D)if(!/^[a-z0-9]+$/.test(F))throw new K(`Invalid tag "${F}". DEV.to tags must be lowercase alphanumeric (no spaces/punctuation).`);return D.join(", ")}export function buildArticle(z,D,F){const H={};if(z.title!==void 0)H.title=String(z.title);if(D!==void 0)H.body_markdown=D;if(z.published!==void 0)H.published=!!z.published;if(F!==void 0)H.tag_list=F;if(z["cover-image"])H.main_image=String(z["cover-image"]);if(z["canonical-url"])H.canonical_url=String(z["canonical-url"]);if(z.series)H.series=String(z.series);if(z.description)H.description=String(z.description);return H}q({site:"devto",name:"publish",access:"write",description:"Publish or update a DEV.to article (markdown). Draft by default; pass --published to go live.",domain:"dev.to",strategy:B.COOKIE,browser:!0,args:[{name:"title",type:"string",required:!1,help:"Article title (required for new articles)"},{name:"body",type:"string",required:!1,help:"Article body in Markdown (or use --body-file)"},{name:"body-file",type:"string",required:!1,help:"Path to a Markdown file for the body"},{name:"published",type:"boolean",required:!1,default:!1,help:"Publish live now (default: save as draft)"},{name:"tags",type:"string",required:!1,help:"Comma-separated tags, max 4, lowercase alphanumeric"},{name:"cover-image",type:"string",required:!1,help:"Cover image URL (main_image)"},{name:"canonical-url",type:"string",required:!1,help:"Canonical URL (original source)"},{name:"series",type:"string",required:!1,help:"Series name to group articles"},{name:"description",type:"string",required:!1,help:"Social/SEO description"},{name:"id",type:"string",required:!1,help:"Existing article id to update (PUT instead of POST)"}],columns:["status","id","url","published","slug"],func:async(z,D)=>{if(!z)throw new O("Browser session required for devto publish");const F=D.id?String(D.id).trim():"",H=resolveBody(D),Y=resolveTagList(D.tags);if(!F&&!D.title)throw new K("--title is required when creating a new article");if(!F&&H===void 0)throw new K("--body or --body-file is required when creating a new article");const P=buildArticle(D,H,Y),Z=F?"PUT":"POST",$=F?`/articles/${encodeURIComponent(F)}`:"/articles";await z.goto("https://dev.to/");const I=await z.evaluate(`(async () => {
2
2
  try {
3
3
  var csrf = (document.querySelector('meta[name="csrf-token"]') || {}).content || window.csrfToken;
4
4
  if (!csrf) return { kind: 'auth', detail: 'no csrf-token meta — not logged in?' };
@@ -14,4 +14,4 @@ import*as V from"node:fs";import{cli as q,Strategy as B}from"@jackwener/opencli/
14
14
  if (!r.ok) return { kind: 'http', status: r.status, detail: text.slice(0, 300) };
15
15
  return { kind: 'ok', data: data };
16
16
  } catch (e) { return { kind: 'exception', detail: String(e && e.message || e) }; }
17
- })()`);if(I?.kind==="auth")throw new U("dev.to",`Not logged in (${I.detail}). Run \`opencli devto login\`.`);if(I?.kind==="http")throw new O(`dev.to publish failed: HTTP ${I.status} ${I.detail}`);if(I?.kind==="exception")throw new O(`dev.to publish error: ${I.detail}`);if(I?.kind!=="ok"||!I.data)throw new O(`Unexpected dev.to response: ${JSON.stringify(I)}`);const J=I.data,Q=!!P.published;let N="";if(J.current_state_path)try{N=new URL(J.current_state_path,"https://dev.to").href}catch(j){N=String(J.current_state_path)}else if(J.url)N=String(J.url);return[{status:F?"updated":"created",id:J.id??"",url:Q?N:"",published:Q,slug:J.slug??""}]}});
17
+ })()`);if(I?.kind==="auth")throw new U("dev.to",`Not logged in (${I.detail}). Run \`opencli devto login\`.`);if(I?.kind==="http")throw new O(`dev.to publish failed: HTTP ${I.status} ${I.detail}`);if(I?.kind==="exception")throw new O(`dev.to publish error: ${I.detail}`);if(I?.kind!=="ok"||!I.data)throw new O(`Unexpected dev.to response: ${JSON.stringify(I)}`);const J=I.data,Q=!!P.published;let N="";if(J.current_state_path)try{N=new URL(J.current_state_path,"https://dev.to").href}catch(G){N=String(J.current_state_path)}else if(J.url)N=String(J.url);return[{status:F?"updated":"created",id:J.id??"",url:Q?N:"",published:Q,slug:J.slug??""}]}});
@@ -1,4 +1,4 @@
1
- import*as M from"node:fs";import{cli as I,Strategy as S}from"@jackwener/opencli/registry";import{ArgumentError as j,AuthRequiredError as b,CommandExecutionError as V}from"@jackwener/opencli/errors";const B="https://hashnode.com";export function resolveBody(z){if(z["body-file"]){const D=String(z["body-file"]);if(!M.statSync(D,{throwIfNoEntry:!1})?.isFile())throw new j(`--body-file not found: ${D}`);return M.readFileSync(D,"utf8")}if(z.body!==void 0&&z.body!==null)return String(z.body);return}export function resolveTags(z){if(z===void 0||z===null||z==="")return[];return[...new Set(String(z).split(",").map((D)=>D.trim()).filter(Boolean))]}const h=`(async () => {
1
+ import*as M from"node:fs";import{cli as I,Strategy as S}from"@jackwener/opencli/registry";import{ArgumentError as _,AuthRequiredError as b,CommandExecutionError as V}from"@jackwener/opencli/errors";import{assertLiteralContent as O}from"../_shared/content-guard.js";const j="https://hashnode.com";export function resolveBody(z){if(z["body-file"]){const D=String(z["body-file"]);if(!M.statSync(D,{throwIfNoEntry:!1})?.isFile())throw new _(`--body-file not found: ${D}`);return M.readFileSync(D,"utf8")}if(z.body!==void 0&&z.body!==null){O(String(z.body),{fileFlag:"--body-file"});return String(z.body)}return}export function resolveTags(z){if(z===void 0||z===null||z==="")return[];return[...new Set(String(z).split(",").map((D)=>D.trim()).filter(Boolean))]}const h=`(async () => {
2
2
  if (location.href.indexOf('hashnode.com') < 0) return { drift: true };
3
3
  try {
4
4
  var r = await fetch('/api/auth/session', { credentials: 'include', headers: { Accept: 'application/json' } });
@@ -6,7 +6,7 @@ import*as M from"node:fs";import{cli as I,Strategy as S}from"@jackwener/opencli/
6
6
  var u = j && j.user;
7
7
  return u && u.username ? { ok: true, username: String(u.username) } : { ok: false };
8
8
  } catch (e) { return { drift: true, err: String(e && e.message || e) }; }
9
- })()`;async function W(z,D,{tries:Y=8,label:Z="eval",anchor:$=B}={}){let q;for(let T=0;T<Y;T+=1){let F;try{F=await z.evaluate(D)}catch(G){F={drift:!0,err:String(G&&G.message||G)}}q=F;if(F&&F.drift){await z.goto($);await z.wait(2);continue}return F}throw new V(`Hashnode ${Z}: browser tab kept drifting (last: ${JSON.stringify(q)})`)}I({site:"hashnode",name:"publish",access:"write",description:"Publish a Hashnode post via the web editor (or save a draft with --draft). Markdown body + tags/cover/canonical.",domain:"hashnode.com",strategy:S.COOKIE,browser:!0,defaultWindowMode:"foreground",siteSession:"persistent",args:[{name:"title",type:"string",required:!0,help:"Post title"},{name:"body",type:"string",required:!1,help:"Post body in Markdown (or use --body-file)"},{name:"body-file",type:"string",required:!1,help:"Path to a Markdown file for the body"},{name:"tags",type:"string",required:!1,help:"Comma-separated tag names"},{name:"cover-image",type:"string",required:!1,help:"Cover image URL"},{name:"canonical-url",type:"string",required:!1,help:"Canonical / original article URL"},{name:"draft",type:"boolean",required:!1,default:!1,help:"Save as draft instead of publishing"}],columns:["status","draft_id","url"],func:async(z,D)=>{if(!z)throw new V("Browser session required for hashnode publish");const Y=String(D.title??"").trim();if(!Y)throw new j("hashnode publish: --title is required");const Z=resolveBody(D);if(Z===void 0)throw new j("--body or --body-file is required");const $=resolveTags(D.tags),q=D["canonical-url"]?String(D["canonical-url"]):"",T=D["cover-image"]?String(D["cover-image"]):"";await z.goto(B);const F=await W(z,h,{label:"auth"});if(!F||!F.ok)throw new b("hashnode.com","Not logged in. Run `opencli hashnode login`.");const G=await W(z,`(async () => {
9
+ })()`;async function W(z,D,{tries:Y=8,label:Z="eval",anchor:$=j}={}){let q;for(let B=0;B<Y;B+=1){let F;try{F=await z.evaluate(D)}catch(G){F={drift:!0,err:String(G&&G.message||G)}}q=F;if(F&&F.drift){await z.goto($);await z.wait(2);continue}return F}throw new V(`Hashnode ${Z}: browser tab kept drifting (last: ${JSON.stringify(q)})`)}I({site:"hashnode",name:"publish",access:"write",description:"Publish a Hashnode post via the web editor (or save a draft with --draft). Markdown body + tags/cover/canonical.",domain:"hashnode.com",strategy:S.COOKIE,browser:!0,defaultWindowMode:"foreground",siteSession:"persistent",args:[{name:"title",type:"string",required:!0,help:"Post title"},{name:"body",type:"string",required:!1,help:"Post body in Markdown (or use --body-file)"},{name:"body-file",type:"string",required:!1,help:"Path to a Markdown file for the body"},{name:"tags",type:"string",required:!1,help:"Comma-separated tag names"},{name:"cover-image",type:"string",required:!1,help:"Cover image URL"},{name:"canonical-url",type:"string",required:!1,help:"Canonical / original article URL"},{name:"draft",type:"boolean",required:!1,default:!1,help:"Save as draft instead of publishing"}],columns:["status","draft_id","url"],func:async(z,D)=>{if(!z)throw new V("Browser session required for hashnode publish");const Y=String(D.title??"").trim();if(!Y)throw new _("hashnode publish: --title is required");const Z=resolveBody(D);if(Z===void 0)throw new _("--body or --body-file is required");const $=resolveTags(D.tags),q=D["canonical-url"]?String(D["canonical-url"]):"",B=D["cover-image"]?String(D["cover-image"]):"";await z.goto(j);const F=await W(z,h,{label:"auth"});if(!F||!F.ok)throw new b("hashnode.com","Not logged in. Run `opencli hashnode login`.");const G=await W(z,`(async () => {
10
10
  if (location.href.indexOf('hashnode.com') < 0) return { drift: true };
11
11
  const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
12
12
  const w = [...document.querySelectorAll('a,button')].find((e) => (e.innerText || '').trim() === 'Write');
@@ -16,7 +16,7 @@ import*as M from"node:fs";import{cli as I,Strategy as S}from"@jackwener/opencli/
16
16
  while (t < 18000) { await sleep(500); t += 500; if (location.href.indexOf('/draft/') >= 0) break; }
17
17
  const m = location.href.match(/\\/draft\\/([a-z0-9]+)/i);
18
18
  return m ? { ok: true, draftId: m[1], href: location.href } : { ok: false, reason: 'editor did not open (no /draft/ url) — does the account have a publication?' };
19
- })()`,{label:"new-draft"});if(!G||!G.ok)throw new V(`Hashnode could not open the editor: ${G?G.reason:"unknown"}`);const _=G.draftId,Q=`${B}/draft/${_}`;await z.goto(Q);const J=await W(z,`(async () => {
19
+ })()`,{label:"new-draft"});if(!G||!G.ok)throw new V(`Hashnode could not open the editor: ${G?G.reason:"unknown"}`);const L=G.draftId,Q=`${j}/draft/${L}`;await z.goto(Q);const J=await W(z,`(async () => {
20
20
  if (location.href.indexOf('/draft/') < 0) return { drift: true };
21
21
  const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
22
22
  let t = 0, title, body;
@@ -36,7 +36,7 @@ import*as M from"node:fs";import{cli as I,Strategy as S}from"@jackwener/opencli/
36
36
  document.execCommand('insertText', false, ${JSON.stringify(Z)});
37
37
  await sleep(1500);
38
38
  return { ok: true, titleVal: title.value, bodyLen: (body.innerText || '').length };
39
- })()`,{label:"fill",anchor:Q});if(!J||!J.ok)throw new V(`Hashnode fill failed: ${J?J.reason:"unknown"}`);if(D.draft)return[{status:"draft",draft_id:_,url:Q}];const A=JSON.stringify($),H=q;await z.goto(Q);const L=await W(z,`(async () => {
39
+ })()`,{label:"fill",anchor:Q});if(!J||!J.ok)throw new V(`Hashnode fill failed: ${J?J.reason:"unknown"}`);if(D.draft)return[{status:"draft",draft_id:L,url:Q}];const A=JSON.stringify($),H=q;await z.goto(Q);const N=await W(z,`(async () => {
40
40
  if (location.href.indexOf('/draft/') < 0) return { drift: true };
41
41
  const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
42
42
  let t = 0, tops;
@@ -47,7 +47,7 @@ import*as M from"node:fs";import{cli as I,Strategy as S}from"@jackwener/opencli/
47
47
  tops.forEach((b) => b.removeAttribute('data-pp'));
48
48
  tops[0].setAttribute('data-pp', 'top-publish');
49
49
  return { ok: true };
50
- })()`,{label:"publish-ready",anchor:Q});if(!L||!L.ok)throw new V(`Hashnode publish not ready: ${L?L.reason:"unknown"}`);await z.click('[data-pp="top-publish"]');await z.wait(3);const X=await W(z,`(async () => {
50
+ })()`,{label:"publish-ready",anchor:Q});if(!N||!N.ok)throw new V(`Hashnode publish not ready: ${N?N.reason:"unknown"}`);await z.click('[data-pp="top-publish"]');await z.wait(3);const X=await W(z,`(async () => {
51
51
  if (location.href.indexOf('hashnode.com') < 0) return { drift: true };
52
52
  const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
53
53
  let d = 0, dlg;
@@ -75,7 +75,7 @@ import*as M from"node:fs";import{cli as I,Strategy as S}from"@jackwener/opencli/
75
75
  await sleep(500);
76
76
  }
77
77
  return { ok: true };
78
- })()`,{label:"tags",anchor:Q})}catch(P){}if(H)try{await W(z,`(async () => {
78
+ })()`,{label:"tags",anchor:Q})}catch(T){}if(H)try{await W(z,`(async () => {
79
79
  if (location.href.indexOf('hashnode.com') < 0) return { drift: true };
80
80
  const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
81
81
  var toggle = [...document.querySelectorAll('label,button')].find((e) => /add a canonical url/i.test(e.innerText || ''));
@@ -85,8 +85,8 @@ import*as M from"node:fs";import{cli as I,Strategy as S}from"@jackwener/opencli/
85
85
  var ci = document.querySelector('input[placeholder="https://example.com/original-article"]');
86
86
  if (ci) { var s = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype,'value').set; ci.focus(); s.call(ci, ${JSON.stringify(H)}); ci.dispatchEvent(new Event('input',{bubbles:true})); ci.dispatchEvent(new Event('blur',{bubbles:true})); }
87
87
  return true;
88
- })()`)}catch(P){}await z.evaluate(`(() => {
88
+ })()`)}catch(T){}await z.evaluate(`(() => {
89
89
  var dlg = document.querySelector('[role="dialog"][data-state="open"]') || document.querySelector('[role="dialog"]');
90
90
  if (dlg) { var pub = [...dlg.querySelectorAll('button')].find((b) => (b.innerText||'').trim()==='Publish'); if (pub) pub.setAttribute('data-pp','dlg-publish'); }
91
91
  return true;
92
- })()`);await z.click('[data-pp="dlg-publish"]');let N="";for(let P=0;P<30;P+=1){await z.wait(1);const K=await z.evaluate("(() => location.href)()").catch(()=>"");if(typeof K==="string"&&K.indexOf("/draft/")<0&&K.indexOf("hashnode.com")<0){N=K;break}if(typeof K==="string"&&K.indexOf("/draft/")<0&&K.indexOf("/edit/")<0&&K.indexOf("hashnode.com")>=0){N=K;break}}if(!N)throw new V("Hashnode publish did not navigate to a live article URL (still on /draft or /edit). The post may have been saved but not published.");return[{status:"published",draft_id:_,url:N}]}});
92
+ })()`);await z.click('[data-pp="dlg-publish"]');let P="";for(let T=0;T<30;T+=1){await z.wait(1);const K=await z.evaluate("(() => location.href)()").catch(()=>"");if(typeof K==="string"&&K.indexOf("/draft/")<0&&K.indexOf("hashnode.com")<0){P=K;break}if(typeof K==="string"&&K.indexOf("/draft/")<0&&K.indexOf("/edit/")<0&&K.indexOf("hashnode.com")>=0){P=K;break}}if(!P)throw new V("Hashnode publish did not navigate to a live article URL (still on /draft or /edit). The post may have been saved but not published.");return[{status:"published",draft_id:L,url:P}]}});
@@ -1,4 +1,4 @@
1
- import{cli as K,Strategy as M}from"@jackwener/opencli/registry";import{EmptyResultError as O}from"@jackwener/opencli/errors";const P="https://juejin.cn/creator/data/content/article/single";K({site:"juejin",name:"stats",access:"read",description:"掘金文章数据(每篇展现/阅读/点赞/评论/收藏/阅读比/热度等全量运营指标)",domain:"juejin.cn",strategy:M.COOKIE,browser:!0,navigateBefore:!1,args:[{name:"limit",type:"number",default:20,help:"返回条数"}],columns:["id","type","title","url","published_at","views","likes","comments","collects","shares","displays","read_ratio","hot_index","category","tags","read_time","extra"],func:async(C,H)=>{const J=Math.max(1,Number(H.limit)||20);await C.goto(P);const z=await C.evaluate(`(async () => {
1
+ import{cli as K,Strategy as M}from"@jackwener/opencli/registry";import{EmptyResultError as O}from"@jackwener/opencli/errors";const P="https://juejin.cn/creator/data/content/article/single";K({site:"juejin",name:"stats",access:"read",description:"掘金文章数据(每篇展现/阅读/点赞/评论/收藏/阅读比/热度等全量运营指标)。首次调用要拉起浏览器会话、偏慢,客户端超时属正常,重试一次即可",domain:"juejin.cn",strategy:M.COOKIE,browser:!0,navigateBefore:!1,args:[{name:"limit",type:"number",default:20,help:"返回条数"}],columns:["id","type","title","url","published_at","views","likes","comments","collects","shares","displays","read_ratio","hot_index","category","tags","read_time","extra"],func:async(C,H)=>{const J=Math.max(1,Number(H.limit)||20);await C.goto(P);const z=await C.evaluate(`(async () => {
2
2
  try {
3
3
  if (!location.href.includes('juejin.cn')) {
4
4
  return { error: '未登录掘金(页面被重定向到 ' + location.href + '),请先在 Chrome 里登录 juejin.cn' };
@@ -1 +1 @@
1
- import{cli as m,Strategy as s}from"@jackwener/opencli/registry";import{ArgumentError as d}from"@jackwener/opencli/errors";import{WWW_BASE as c,postForm as u,requireOk as a,resolveMomentId as p}from"./utils.js";m({site:"nowcoder",name:"comment",access:"write",description:"评论帖子;配 --to-user/--to-comment 可定向回复某条评论",domain:"www.nowcoder.com",strategy:s.COOKIE,args:[{name:"id",positional:!0,required:!0,help:"帖子 id(数字 momentId / UUID / 链接均可)"},{name:"content",positional:!0,required:!0,help:"评论内容"},{name:"to-user",type:"str",default:"0",help:"帖主/对方 userId(定向回复时必传)"},{name:"to-comment",type:"str",default:"0",help:"定向回复的评论 id(inbox 的 reply_comment_id)"}],columns:["post_id","kind","status"],func:async(n,t)=>{const r=await p(n,t.id),e=String(t.content??"").trim();if(!e)throw new d("评论内容不能为空");const i=String(t["to-user"]??"0").trim()||"0",o=String(t["to-comment"]??"0").trim()||"0";a(await u(n,`${c}/comment/create?_=${Date.now()}`,{entityType:"74",entityId:r,commentContent:e,contentV2:JSON.stringify({pureText:e,imgs:[]}),toFeed:"false",toUserId:i,toCommentId:o,entityOwnerId:i,isAnonymousFlag:"false"}),"评论");return[{post_id:r,kind:o!=="0"?`reply-to-${o}`:"comment",status:"ok"}]}});
1
+ import{cli as m,Strategy as s}from"@jackwener/opencli/registry";import{ArgumentError as d}from"@jackwener/opencli/errors";import{WWW_BASE as c,postForm as u,requireOk as a,resolveMomentId as p}from"./utils.js";import{assertLiteralContent as l}from"../_shared/content-guard.js";m({site:"nowcoder",name:"comment",access:"write",description:"评论帖子;配 --to-user/--to-comment 可定向回复某条评论",domain:"www.nowcoder.com",strategy:s.COOKIE,args:[{name:"id",positional:!0,required:!0,help:"帖子 id(数字 momentId / UUID / 链接均可)"},{name:"content",positional:!0,required:!0,help:"评论内容"},{name:"to-user",type:"str",default:"0",help:"帖主/对方 userId(定向回复时必传)"},{name:"to-comment",type:"str",default:"0",help:"定向回复的评论 id(inbox 的 reply_comment_id)"}],columns:["post_id","kind","status"],func:async(n,t)=>{const r=await p(n,t.id),e=String(t.content??"").trim();l(e,{label:"评论内容"});if(!e)throw new d("评论内容不能为空");const i=String(t["to-user"]??"0").trim()||"0",o=String(t["to-comment"]??"0").trim()||"0";a(await u(n,`${c}/comment/create?_=${Date.now()}`,{entityType:"74",entityId:r,commentContent:e,contentV2:JSON.stringify({pureText:e,imgs:[]}),toFeed:"false",toUserId:i,toCommentId:o,entityOwnerId:i,isAnonymousFlag:"false"}),"评论");return[{post_id:r,kind:o!=="0"?`reply-to-${o}`:"comment",status:"ok"}]}});
@@ -1 +1 @@
1
- import{cli as r,Strategy as i}from"@jackwener/opencli/registry";import{ArgumentError as s}from"@jackwener/opencli/errors";import{GW_BASE as c,postJson as a,requireOk as d}from"./utils.js";r({site:"nowcoder",name:"dm-send",access:"write",description:"发私信(conversation 为 dm-list 的 conversation_id)",domain:"www.nowcoder.com",strategy:i.COOKIE,args:[{name:"conversation",positional:!0,required:!0,help:"会话 id(dm-list 的 conversation_id,格式 <我uid>_<对方uid>)"},{name:"content",positional:!0,required:!0,help:"私信内容"}],columns:["conversation_id","status"],func:async(e,o)=>{const t=String(o.conversation??"").trim(),n=String(o.content??"").trim();if(!n)throw new s("私信内容不能为空");d(await a(e,`${c}/msg/send-msg`,{body:{content:n,sourceType:1200,conversationId:t}}),"私信发送");return[{conversation_id:t,status:"sent"}]}});
1
+ import{cli as r,Strategy as i}from"@jackwener/opencli/registry";import{ArgumentError as s}from"@jackwener/opencli/errors";import{GW_BASE as a,postJson as c,requireOk as d}from"./utils.js";import{assertLiteralContent as m}from"../_shared/content-guard.js";r({site:"nowcoder",name:"dm-send",access:"write",description:"发私信(conversation 为 dm-list 的 conversation_id)",domain:"www.nowcoder.com",strategy:i.COOKIE,args:[{name:"conversation",positional:!0,required:!0,help:"会话 id(dm-list 的 conversation_id,格式 <我uid>_<对方uid>)"},{name:"content",positional:!0,required:!0,help:"私信内容"}],columns:["conversation_id","status"],func:async(n,o)=>{const e=String(o.conversation??"").trim(),t=String(o.content??"").trim();m(t,{label:"私信内容"});if(!t)throw new s("私信内容不能为空");d(await c(n,`${a}/msg/send-msg`,{body:{content:t,sourceType:1200,conversationId:e}}),"私信发送");return[{conversation_id:e,status:"sent"}]}});
@@ -1 +1 @@
1
- import{cli as i,Strategy as c}from"@jackwener/opencli/registry";import{ArgumentError as a,CommandExecutionError as m}from"@jackwener/opencli/errors";import{GW_BASE as l,postJson as s,requireOk as d}from"./utils.js";i({site:"nowcoder",name:"post",access:"write",description:"发布动态(实名发帖,高风险写操作,务必低频且内容有价值)",domain:"www.nowcoder.com",strategy:c.COOKIE,args:[{name:"content",positional:!0,required:!0,help:"正文内容"},{name:"title",type:"str",default:"",help:"标题(可选)"}],columns:["moment_id","content_id","url"],func:async(r,e)=>{const n=String(e.content??"").trim();if(!n)throw new a("发帖正文不能为空");const o=d(await s(r,`${l}/moment/create`,{title:String(e.title??""),content:n,type:0,isAnonymousFlag:!1,publicEntrance:"发动态",publicEntrancePage:"首页"}),"发帖"),t=o.data??{};if(!t.id)throw new m(`牛客发帖响应缺少 momentId: ${JSON.stringify(o).slice(0,200)}`);return[{moment_id:t.id,content_id:t.result??"",url:t.result?`https://www.nowcoder.com/feed/main/detail/${t.result}`:""}]}});
1
+ import{cli as i,Strategy as a}from"@jackwener/opencli/registry";import{ArgumentError as c,CommandExecutionError as m}from"@jackwener/opencli/errors";import{GW_BASE as l,postJson as s,requireOk as d}from"./utils.js";import{assertLiteralContent as p}from"../_shared/content-guard.js";i({site:"nowcoder",name:"post",access:"write",description:"发布动态(实名发帖,高风险写操作,务必低频且内容有价值)",domain:"www.nowcoder.com",strategy:a.COOKIE,args:[{name:"content",positional:!0,required:!0,help:"正文内容"},{name:"title",type:"str",default:"",help:"标题(可选)"}],columns:["moment_id","content_id","url"],func:async(o,n)=>{const e=String(n.content??"").trim();p(e,{label:"正文内容"});if(!e)throw new c("发帖正文不能为空");const r=d(await s(o,`${l}/moment/create`,{title:String(n.title??""),content:e,type:0,isAnonymousFlag:!1,publicEntrance:"发动态",publicEntrancePage:"首页"}),"发帖"),t=r.data??{};if(!t.id)throw new m(`牛客发帖响应缺少 momentId: ${JSON.stringify(r).slice(0,200)}`);return[{moment_id:t.id,content_id:t.result??"",url:t.result?`https://www.nowcoder.com/feed/main/detail/${t.result}`:""}]}});
@@ -1,7 +1,7 @@
1
- import*as $ from"node:fs";import{cli as V,Strategy as v}from"@jackwener/opencli/registry";import{ArgumentError as Q,CommandExecutionError as W}from"@jackwener/opencli/errors";import{qiitaGql as Y,qiitaDeleteByForm as H}from"./gql.js";export function resolveBody(h){if(h["body-file"]){const z=String(h["body-file"]);if(!$.statSync(z,{throwIfNoEntry:!1})?.isFile())throw new Q(`--body-file not found: ${z}`);return $.readFileSync(z,"utf8")}if(h.body!==void 0&&h.body!==null)return String(h.body);return}export function resolveTagNames(h){return String(h??"").split(",").map((z)=>z.trim()).filter(Boolean)}const R=`mutation($input: SaveCreatingArticleInput!) {
1
+ import*as h from"node:fs";import{cli as V,Strategy as v}from"@jackwener/opencli/registry";import{ArgumentError as W,CommandExecutionError as X}from"@jackwener/opencli/errors";import{qiitaGql as Z,qiitaDeleteByForm as H}from"./gql.js";import{assertLiteralContent as R}from"../_shared/content-guard.js";export function resolveBody(z){if(z["body-file"]){const J=String(z["body-file"]);if(!h.statSync(J,{throwIfNoEntry:!1})?.isFile())throw new W(`--body-file not found: ${J}`);return h.readFileSync(J,"utf8")}if(z.body!==void 0&&z.body!==null){R(String(z.body),{fileFlag:"--body-file"});return String(z.body)}return}export function resolveTagNames(z){return String(z??"").split(",").map((J)=>J.trim()).filter(Boolean)}const G=`mutation($input: SaveCreatingArticleInput!) {
2
2
  saveCreatingArticle(input: $input) { draftItem { uuid } }
3
- }`,j=`mutation($input: PublishPublicArticleInput!) {
3
+ }`,I=`mutation($input: PublishPublicArticleInput!) {
4
4
  publishPublicArticle(input: $input) { article { uuid linkUrl encryptedId isSecret } }
5
- }`,G=`mutation($input: PublishSecretArticleInput!) {
5
+ }`,P=`mutation($input: PublishSecretArticleInput!) {
6
6
  publishSecretArticle(input: $input) { article { uuid linkUrl encryptedId isSecret } }
7
- }`;V({site:"qiita",name:"publish",access:"write",description:"Publish a Qiita article (markdown). Tags required. Public by default; --draft or --secret to change.",domain:"qiita.com",strategy:v.COOKIE,browser:!0,args:[{name:"title",type:"string",required:!1,help:"Article title (required)"},{name:"body",type:"string",required:!1,help:"Article body in Markdown (or use --body-file)"},{name:"body-file",type:"string",required:!1,help:"Path to a Markdown file for the body"},{name:"tags",type:"string",required:!1,help:"Comma-separated tags (REQUIRED, at least 1)"},{name:"draft",type:"boolean",required:!1,default:!1,help:"Save as draft only (do not publish)"},{name:"secret",type:"boolean",required:!1,default:!1,help:"Publish as limited-public (secret) instead of public"},{name:"tweet-share",type:"boolean",required:!1,default:!1,help:"Also share to X/Twitter on publish (public only)"}],columns:["status","uuid","url"],func:async(h,z)=>{if(!h)throw new W("Browser session required for qiita publish");const J=String(z.title??"").trim(),K=resolveBody(z),X=resolveTagNames(z.tags);if(!J)throw new Q("--title is required");if(K===void 0)throw new Q("--body or --body-file is required");if(X.length===0)throw new Q("--tags is required (Qiita articles need at least one tag)");await h.goto("https://qiita.com/drafts/new");await h.wait({time:2});const Z=await Y(h,R,{input:{uuid:"",title:J,rawBody:K,tagNames:X,slide:!1,organizationId:null}}),O=Z?.saveCreatingArticle?.draftItem?.uuid;if(!O)throw new W(`Qiita saveCreatingArticle returned no uuid: ${JSON.stringify(Z)}`);if(z.draft)return[{status:"draft",uuid:O,url:`https://qiita.com/drafts/${O}/edit`}];let M;if(z.secret)M=(await Y(h,G,{input:{uuid:O,title:J,rawBody:K,tagNames:X,slide:!1}}))?.publishSecretArticle?.article;else M=(await Y(h,j,{input:{uuid:O,title:J,rawBody:K,tagNames:X,slide:!1,tweetShare:!!z["tweet-share"],adventCalendarItems:[]}}))?.publishPublicArticle?.article;if(!M||!M.linkUrl)throw new W(`Qiita publish returned no article: ${JSON.stringify(M)}`);return[{status:z.secret?"published-secret":"published",uuid:M.uuid||O,url:M.linkUrl}]}});V({site:"qiita",name:"delete",access:"write",description:"Delete a Qiita article or draft by its URL (article linkUrl, or https://qiita.com/drafts/<uuid>).",domain:"qiita.com",strategy:v.COOKIE,browser:!0,args:[{name:"url",type:"string",required:!1,help:"Article URL (e.g. https://qiita.com/<urlName>/items/<uuid>) or draft URL (https://qiita.com/drafts/<uuid>)"}],columns:["status","url"],func:async(h,z)=>{if(!h)throw new W("Browser session required for qiita delete");const J=String(z.url??"").trim();if(!/^https:\/\/qiita\.com\/.+/.test(J))throw new Q("--url is required (a https://qiita.com/... article or draft URL)");await h.goto("https://qiita.com/");await h.wait({time:1});const K=await H(h,J);if(!K.ok)throw new W(`Qiita delete failed (HTTP ${K.status}) for ${J}`);return[{status:"deleted",url:J}]}});
7
+ }`;V({site:"qiita",name:"publish",access:"write",description:"Publish a Qiita article (markdown). Tags required. Public by default; --draft or --secret to change.",domain:"qiita.com",strategy:v.COOKIE,browser:!0,args:[{name:"title",type:"string",required:!1,help:"Article title (required)"},{name:"body",type:"string",required:!1,help:"Article body in Markdown (or use --body-file)"},{name:"body-file",type:"string",required:!1,help:"Path to a Markdown file for the body"},{name:"tags",type:"string",required:!1,help:"Comma-separated tags (REQUIRED, at least 1)"},{name:"draft",type:"boolean",required:!1,default:!1,help:"Save as draft only (do not publish)"},{name:"secret",type:"boolean",required:!1,default:!1,help:"Publish as limited-public (secret) instead of public"},{name:"tweet-share",type:"boolean",required:!1,default:!1,help:"Also share to X/Twitter on publish (public only)"}],columns:["status","uuid","url"],func:async(z,J)=>{if(!z)throw new X("Browser session required for qiita publish");const K=String(J.title??"").trim(),M=resolveBody(J),Y=resolveTagNames(J.tags);if(!K)throw new W("--title is required");if(M===void 0)throw new W("--body or --body-file is required");if(Y.length===0)throw new W("--tags is required (Qiita articles need at least one tag)");await z.goto("https://qiita.com/drafts/new");await z.wait({time:2});const $=await Z(z,G,{input:{uuid:"",title:K,rawBody:M,tagNames:Y,slide:!1,organizationId:null}}),Q=$?.saveCreatingArticle?.draftItem?.uuid;if(!Q)throw new X(`Qiita saveCreatingArticle returned no uuid: ${JSON.stringify($)}`);if(J.draft)return[{status:"draft",uuid:Q,url:`https://qiita.com/drafts/${Q}/edit`}];let O;if(J.secret)O=(await Z(z,P,{input:{uuid:Q,title:K,rawBody:M,tagNames:Y,slide:!1}}))?.publishSecretArticle?.article;else O=(await Z(z,I,{input:{uuid:Q,title:K,rawBody:M,tagNames:Y,slide:!1,tweetShare:!!J["tweet-share"],adventCalendarItems:[]}}))?.publishPublicArticle?.article;if(!O||!O.linkUrl)throw new X(`Qiita publish returned no article: ${JSON.stringify(O)}`);return[{status:J.secret?"published-secret":"published",uuid:O.uuid||Q,url:O.linkUrl}]}});V({site:"qiita",name:"delete",access:"write",description:"Delete a Qiita article or draft by its URL (article linkUrl, or https://qiita.com/drafts/<uuid>).",domain:"qiita.com",strategy:v.COOKIE,browser:!0,args:[{name:"url",type:"string",required:!1,help:"Article URL (e.g. https://qiita.com/<urlName>/items/<uuid>) or draft URL (https://qiita.com/drafts/<uuid>)"}],columns:["status","url"],func:async(z,J)=>{if(!z)throw new X("Browser session required for qiita delete");const K=String(J.url??"").trim();if(!/^https:\/\/qiita\.com\/.+/.test(K))throw new W("--url is required (a https://qiita.com/... article or draft URL)");await z.goto("https://qiita.com/");await z.wait({time:1});const M=await H(z,K);if(!M.ok)throw new X(`Qiita delete failed (HTTP ${M.status}) for ${K}`);return[{status:"deleted",url:K}]}});
@@ -0,0 +1,43 @@
1
+ export const SF_SIGNED_GET_JS=`
2
+ async function __sfSignedGet(path) {
3
+ let srvSec = null;
4
+ try {
5
+ const h = await fetch("/", { method: "HEAD" });
6
+ const dh = h.headers.get("date");
7
+ if (dh) { const t = new Date(dh).getTime(); if (!isNaN(t)) srvSec = Math.floor(t / 1000); }
8
+ } catch (e) {}
9
+ const key = Object.keys(window).find((k) => k.startsWith("webpackChunk"));
10
+ if (!key) return { __sfErr: "页面未就绪:未找到 webpack 运行时,无法复用思否请求签名(请确认已停在 segmentfault.com 页面)" };
11
+ let req;
12
+ try { window[key].push([[Symbol("pp_sf_" + Math.random())], {}, (r) => { req = r; }]); }
13
+ catch (e) { return { __sfErr: "注入 webpack 探针失败:" + String(e) }; }
14
+ if (!req || !req.m) return { __sfErr: "无法取得 webpack require(思否前端结构可能已变)" };
15
+ let modId = null;
16
+ for (const id of Object.keys(req.m)) {
17
+ let s = "";
18
+ try { s = String(req.m[id]); } catch (e) { continue; }
19
+ if (/function getUrl/.test(s) && s.indexOf("/gateway") !== -1) { modId = id; break; }
20
+ }
21
+ if (!modId) return { __sfErr: "未定位到思否请求签名模块(getUrl/gateway),站点前端结构可能已变,需更新 adapter" };
22
+ let RequestClass;
23
+ try { RequestClass = req(modId).default; } catch (e) { return { __sfErr: "加载思否请求模块失败:" + String(e) }; }
24
+ if (typeof RequestClass !== "function") return { __sfErr: "思否请求模块导出异常(default 非构造器)" };
25
+ let xhr = null;
26
+ const OrigOpen = XMLHttpRequest.prototype.open;
27
+ XMLHttpRequest.prototype.open = function (method, url) {
28
+ try { if (String(url).indexOf(path) !== -1) xhr = this; } catch (e) {}
29
+ return OrigOpen.apply(this, arguments);
30
+ };
31
+ try {
32
+ const inst = new RequestClass({ Token: "", userAgent: navigator.userAgent });
33
+ if (srvSec) inst.setServerTime(srvSec);
34
+ await inst.send(path, { method: "GET" }).catch(function () {});
35
+ } catch (e) {
36
+ XMLHttpRequest.prototype.open = OrigOpen;
37
+ return { __sfErr: "签名请求执行失败:" + String(e) };
38
+ }
39
+ XMLHttpRequest.prototype.open = OrigOpen;
40
+ if (!xhr) return { __sfErr: "签名请求未发出(Request.send 未触发 XHR,思否前端结构可能已变)" };
41
+ return { status: xhr.status, raw: String(xhr.responseText || "") };
42
+ }
43
+ `;
@@ -1,3 +1,3 @@
1
- import{CliError as H,CommandExecutionError as F}from"@jackwener/opencli/errors";import{cli as R,Strategy as b}from"@jackwener/opencli/registry";import{publishArticle as S}from"../_shared/article/publish.js";import{readFile as P,stat as N}from"node:fs/promises";function v(V){if(!V.execute)throw new H("INVALID_INPUT","本命令需要 --execute 才会真正发布,请确认后加上 --execute")}async function C(V){const Z=typeof V.text==="string"?V.text:void 0,Q=typeof V.file==="string"?V.file:void 0;if(Z&&Q)throw new H("INVALID_INPUT","请只传 <text> 或 --file 之一,不能同时使用");let M=Z??"";if(Q){let Y;try{Y=await N(Q)}catch{throw new H("INVALID_INPUT",`文件不存在:${Q}`)}if(!Y.isFile())throw new H("INVALID_INPUT",`路径必须是可读文本文件:${Q}`);let A;try{A=await P(Q)}catch{throw new H("INVALID_INPUT",`无法读取文件:${Q}`)}try{M=new TextDecoder("utf-8",{fatal:!0}).decode(A)}catch{throw new H("INVALID_INPUT",`文件编码不是 UTF-8:${Q}`)}}if(!M.trim())throw new H("INVALID_INPUT","正文不能为空");return M}function y(V,Z,Q,M,Y={}){return[{status:"success",outcome:M,message:V,target_type:Z,target:Q,...Y}]}export const segmentfaultProfile={home:"https://segmentfault.com/write",outputFormat:"markdown",image:{skip:["image-static.segmentfault.com","avatar-static.segmentfault.com"],uploadFn:async(V,Z)=>{const M=await(await fetch("https://segmentfault.com/write",{credentials:"include"})).text();let Y=null;const A=M.match(/serverData"\s*:\s*\{\s*"Token"\s*:\s*"([^"]+)"/);if(A)Y=A[1];else{const L="window.g_initialProps = ",q=M.indexOf(L);if(q!==-1){const W=M.indexOf(`;
2
- </script>`,q);if(W!==-1)try{const B=JSON.parse(M.substring(q+L.length,W));Y=B&&B.global&&B.global.sessionInfo&&B.global.sessionInfo.key}catch(B){}}}if(!Y)throw Error("获取思否 session token 失败");const j=await fetch(V,{credentials:"omit"});if(!j.ok)throw Error("下载图片失败:"+j.status);const D=await j.blob(),U=new FormData;U.append("image",D);const _=await(await fetch("https://segmentfault.com/gateway/image",{method:"POST",credentials:"include",headers:{token:Y},body:U})).text();if(_==="Unauthorized"||_.includes("禁言")||_.includes("锁定"))throw Error(_==="Unauthorized"?"未授权,请检查登录态":_);let X;try{X=JSON.parse(_)}catch(L){throw Error("图片上传失败:"+_)}let T=null;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(V,Z)=>{const Q=V.params||{},Y=await(await fetch("https://segmentfault.com/write",{credentials:"include"})).text();let A=null;const j=Y.match(/serverData"\s*:\s*\{\s*"Token"\s*:\s*"([^"]+)"/);if(j)A=j[1];else{const G="window.g_initialProps = ",J=Y.indexOf(G);if(J!==-1){const K=Y.indexOf(`;
3
- </script>`,J);if(K!==-1)try{const z=JSON.parse(Y.substring(J+G.length,K));A=z&&z.global&&z.global.sessionInfo&&z.global.sessionInfo.key}catch(z){}}}if(!A)return{ok:!1,stage:"token",status:0,message:"获取思否 session token 失败,请确认已登录"};if(V.draftOnly){const G={title:V.title,tags:[],text:V.content,object_id:"",type:"article"},J=await fetch("https://segmentfault.com/gateway/draft",{method:"POST",credentials:"include",headers:{"Content-Type":"application/json",token:A,accept:"*/*"},body:JSON.stringify(G)}),K=await J.text();if(K==="Unauthorized"||K.includes("禁言")||K.includes("锁定"))return{ok:!1,stage:"publish",status:J.status,message:K==="Unauthorized"?"未授权":K};let z;try{z=JSON.parse(K)}catch($){return{ok:!1,stage:"publish",status:J.status,message:"解析响应失败:"+K.slice(0,200)}}if(Array.isArray(z)){if(z[0]===1)return{ok:!1,stage:"publish",status:J.status,message:z[1]||"发布失败"};const $=z[1];if($&&$.id)return{ok:!0,draft:!0,id:String($.id),url:"https://segmentfault.com/write?draftId="+$.id}}if(!z||!z.id){const $=z&&(z.message||z.msg||z.error||z.errMsg)||K.slice(0,200);return{ok:!1,stage:"publish",status:J.status,message:$}}return{ok:!0,draft:!0,id:String(z.id),url:"https://segmentfault.com/write?draftId="+z.id}}const D={"Content-Type":"application/json",token:A,accept:"*/*"},U=Array.isArray(Q.tags)?Q.tags.map((G)=>String(G).trim()).filter(Boolean):[];if(U.length===0)return{ok:!1,stage:"tags",message:"思否发文必须至少指定 1 个标签(--tags),合法标签可用 `segmentfault tags <名称>` 校验"};const O=[];for(const G of U){let J;try{const z=await fetch("https://segmentfault.com/gateway/tag/"+encodeURIComponent(G),{credentials:"include",headers:D}),$=await z.text();try{J=JSON.parse($)}catch(E){return{ok:!1,stage:"tags",status:z.status,message:"解析标签「"+G+"」响应失败:"+$.slice(0,200)}}}catch(z){return{ok:!1,stage:"tags",message:"解析标签「"+G+"」失败:"+String(z&&z.message||z)}}const K=J&&J.tag;if(!K||K.id==null)return{ok:!1,stage:"tags",message:"思否上找不到标签「"+G+"」,请用 `segmentfault tags <名称>` 确认存在再发布"};O.push(K.id)}const _=Array.isArray(Q.channels)?Q.channels.map((G)=>String(G).trim()).filter(Boolean):[],X=[];if(_.length>0){let G=[];try{const J=await fetch("https://segmentfault.com/gateway/channels",{credentials:"include",headers:D}),K=await J.text();let z;try{z=JSON.parse(K)}catch($){return{ok:!1,stage:"channels",status:J.status,message:"解析频道列表失败:"+K.slice(0,200)}}if(Array.isArray(z))G=z;else if(z&&Array.isArray(z.data))G=z.data;else if(z&&Array.isArray(z.rows))G=z.rows;else if(z&&Array.isArray(z.channels))G=z.channels;else G=[]}catch(J){return{ok:!1,stage:"channels",message:"获取思否频道列表失败:"+String(J&&J.message||J)}}for(const J of _){const K=G.find((z)=>z&&String(z.name||z.title||z.text)===J);if(!K||K.id==null){const z=G.map(($)=>$&&($.name||$.title||$.text)||"").filter(Boolean).join(" / ");return{ok:!1,stage:"channels",message:"思否上找不到频道「"+J+"」,可选:"+(z||"(频道列表为空)")}}X.push(K.id)}}const T={title:V.title,text:V.content,tags:O};if(X.length>0)T.channel=X;if(Q.cover)T.cover=String(Q.cover);const L=await fetch("https://segmentfault.com/gateway/article",{method:"POST",credentials:"include",headers:D,body:JSON.stringify(T)}),q=await L.text();if(q==="Unauthorized"||q.includes("禁言")||q.includes("锁定"))return{ok:!1,stage:"publish",status:L.status,message:q==="Unauthorized"?"未授权,请检查登录态":q};let W;try{W=JSON.parse(q)}catch(G){return{ok:!1,stage:"publish",status:L.status,message:"解析发布响应失败:"+q.slice(0,200)}}if(W&&(W.isError||W.error||W.errno||W.scene_id)){let G=W.message||W.msg||W.error||"";if(W.scene_id)G=(G?G+";":"")+"思否要求人机验证(geetest),无法在自动化环境完成";return{ok:!1,stage:"publish",status:L.status,message:G||"发布失败:"+q.slice(0,200)}}const B=W&&W.data&&W.data.id;if(B==null)return{ok:!1,stage:"publish",status:L.status,message:W&&(W.message||W.msg)||"发布响应缺少文章 id:"+q.slice(0,200)};return{ok:!0,draft:!1,id:String(B),url:"https://segmentfault.com/a/"+B}}};R({site:"segmentfault",name:"article",access:"write",description:"发布文章到思否(SegmentFault)。默认正式发布,加 --draft 仅存草稿。正文默认 Markdown;外链图片自动转存到思否图床。正式发布必须指定标签(--tags),合法标签用 `segmentfault tags <名称>` 校验、频道用 `segmentfault channels` 列举。",domain:"segmentfault.com",strategy:b.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(选填)。需先把图片上传到思否图床得到 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(V,Z)=>{if(!V)throw new F("思否发布需要浏览器会话");v(Z);const Q=String(Z.title??"").trim();if(!Q)throw new H("INVALID_INPUT","文章标题不能为空");const M=await C(Z),Y=Boolean(Z.draft),A=(L)=>typeof L==="string"?L.split(",").map((q)=>q.trim()).filter(Boolean):[],j=A(Z.tags),D=A(Z.channels),U=typeof Z.cover==="string"?Z.cover.trim():"";if(!Y&&j.length===0)throw new H("INVALID_INPUT","思否正式发布必须指定 --tags(至少 1 个标签);合法标签用 `segmentfault tags <名称>` 校验,或加 --draft 只存草稿");const O=await S(V,{title:Q,body:M,format:Z.html?"html":"markdown",draftOnly:Y,profile:segmentfaultProfile,publishParams:{tags:j,channels:D,cover:U}}),_=O.images.uploaded.length|0,X=O.images.failed.length|0;let T=O.draft?"已保存到思否草稿箱(需在网页上手动发布)":"已正式发布到思否";if(_||X)T+=`;图片:${_} 张转存成功${X?`,${X} 张失败`:""}`;return y(T,"article","",O.draft?"draft":"created",{created_target:(O.draft?"draft:":"article:")+O.id,created_url:O.url})}});
1
+ import{CliError as O,CommandExecutionError as v}from"@jackwener/opencli/errors";import{cli as y,Strategy as C}from"@jackwener/opencli/registry";import{publishArticle as p}from"../_shared/article/publish.js";import{readFile as f,stat as u}from"node:fs/promises";function I(X){if(!X.execute)throw new O("INVALID_INPUT","本命令需要 --execute 才会真正发布,请确认后加上 --execute")}async function h(X){const A=typeof X.text==="string"?X.text:void 0,W=typeof X.file==="string"?X.file:void 0;if(A&&W)throw new O("INVALID_INPUT","请只传 <text> 或 --file 之一,不能同时使用");let G=A??"";if(W){let Z;try{Z=await u(W)}catch{throw new O("INVALID_INPUT",`文件不存在:${W}`)}if(!Z.isFile())throw new O("INVALID_INPUT",`路径必须是可读文本文件:${W}`);let M;try{M=await f(W)}catch{throw new O("INVALID_INPUT",`无法读取文件:${W}`)}try{G=new TextDecoder("utf-8",{fatal:!0}).decode(M)}catch{throw new O("INVALID_INPUT",`文件编码不是 UTF-8:${W}`)}}if(!G.trim())throw new O("INVALID_INPUT","正文不能为空");return G}function w(X,A,W,G,Z={}){return[{status:"success",outcome:G,message:X,target_type:A,target:W,...Z}]}export const segmentfaultProfile={home:"https://segmentfault.com/write",outputFormat:"markdown",image:{skip:["image-static.segmentfault.com","avatar-static.segmentfault.com"],uploadFn:async(X,A)=>{const G=await(await fetch("https://segmentfault.com/write",{credentials:"include"})).text();let Z=null;const M=G.match(/serverData"\s*:\s*\{\s*"Token"\s*:\s*"([^"]+)"/);if(M)Z=M[1];else{const _="window.g_initialProps = ",U=G.indexOf(_);if(U!==-1){const T=G.indexOf(`;
2
+ </script>`,U);if(T!==-1)try{const V=JSON.parse(G.substring(U+_.length,T));Z=V&&V.global&&V.global.sessionInfo&&V.global.sessionInfo.key}catch(V){}}}if(!Z)throw Error("获取思否 session token 失败");const b=await fetch(X,{credentials:"omit"});if(!b.ok)throw Error("下载图片失败:"+b.status);const R=await b.blob(),N=new FormData;N.append("image",R);const L=await(await fetch("https://segmentfault.com/gateway/image",{method:"POST",credentials:"include",headers:{token:Z},body:N})).text();if(L==="Unauthorized"||L.includes("禁言")||L.includes("锁定"))throw Error(L==="Unauthorized"?"未授权,请检查登录态":L);let $;try{$=JSON.parse(L)}catch(_){throw Error("图片上传失败:"+L)}let B=null;if($&&$.result)B=$.result;else if(Array.isArray($)){if($[0]===1)throw Error($[1]||"图片上传失败");B=$[1]||($[2]?"https://image-static.segmentfault.com/"+$[2]:null)}if(!B)throw Error("图片上传失败,服务端未返回 URL");return{url:B}}},publish:async(X,A)=>{const W=X.params||{},Z=await(await fetch("https://segmentfault.com/write",{credentials:"include"})).text();let M=null;const b=Z.match(/serverData"\s*:\s*\{\s*"Token"\s*:\s*"([^"]+)"/);if(b)M=b[1];else{const Q="window.g_initialProps = ",z=Z.indexOf(Q);if(z!==-1){const J=Z.indexOf(`;
3
+ </script>`,z);if(J!==-1)try{const K=JSON.parse(Z.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 失败,请确认已登录"};async function R(Q){let z=null;try{const P=(await fetch("/",{method:"HEAD"})).headers.get("date");if(P){const q=new Date(P).getTime();if(!isNaN(q))z=Math.floor(q/1000)}}catch(Y){}const J=Object.keys(window).find((Y)=>Y.startsWith("webpackChunk"));if(!J)return{__sfErr:"页面未就绪:未找到 webpack 运行时,无法复用思否请求签名"};let K;try{window[J].push([[Symbol("pp_sf_"+Math.random())],{},(Y)=>{K=Y}])}catch(Y){return{__sfErr:"注入 webpack 探针失败:"+String(Y)}}if(!K||!K.m)return{__sfErr:"无法取得 webpack require(思否前端结构可能已变)"};let H=null;for(const Y of Object.keys(K.m)){let P="";try{P=String(K.m[Y])}catch(q){continue}if(/function getUrl/.test(P)&&P.indexOf("/gateway")!==-1){H=Y;break}}if(!H)return{__sfErr:"未定位到思否请求签名模块(getUrl/gateway),站点前端结构可能已变,需更新 adapter"};let F;try{F=K(H).default}catch(Y){return{__sfErr:"加载思否请求模块失败:"+String(Y)}}if(typeof F!=="function")return{__sfErr:"思否请求模块导出异常(default 非构造器)"};let D=null;const S=XMLHttpRequest.prototype.open;XMLHttpRequest.prototype.open=function(Y,P){try{if(String(P).indexOf(Q)!==-1)D=this}catch(q){}return S.apply(this,arguments)};try{const Y=new F({Token:M,userAgent:navigator.userAgent});if(z)Y.setServerTime(z);await Y.send(Q,{method:"GET"}).catch(function(){})}catch(Y){XMLHttpRequest.prototype.open=S;return{__sfErr:"签名请求执行失败:"+String(Y)}}XMLHttpRequest.prototype.open=S;if(!D)return{__sfErr:"签名请求未发出(Request.send 未触发 XHR)"};return{status:D.status,raw:String(D.responseText||"")}}if(X.draftOnly){const Q={title:X.title,tags:[],text:X.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(H){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 H=K[1];if(H&&H.id)return{ok:!0,draft:!0,id:String(H.id),url:"https://segmentfault.com/write?draftId="+H.id}}if(!K||!K.id){const H=K&&(K.message||K.msg||K.error||K.errMsg)||J.slice(0,200);return{ok:!1,stage:"publish",status:z.status,message:H}}return{ok:!0,draft:!0,id:String(K.id),url:"https://segmentfault.com/write?draftId="+K.id}}const N={"Content-Type":"application/json",token:M,accept:"*/*"},j=Array.isArray(W.tags)?W.tags.map((Q)=>String(Q).trim()).filter(Boolean):[];if(j.length===0)return{ok:!1,stage:"tags",message:"思否发文必须至少指定 1 个标签(--tags),合法标签可用 `segmentfault tags <名称>` 校验"};const L=[];for(const Q of j){const z=await R("/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(H){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 <名称>` 确认存在再发布"};L.push(K.id)}const $=Array.isArray(W.channels)?W.channels.map((Q)=>String(Q).trim()).filter(Boolean):[],B=[];if($.length>0){let Q=[];const z=await R("/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 $){const H=Q.find((F)=>F&&(String(F.name||F.title||F.text)===K||String(F.slug)===K));if(!H||!H.slug){const F=Q.map((D)=>D&&(D.name||D.title||D.text)||"").filter(Boolean).join(" / ");return{ok:!1,stage:"channels",message:"思否上找不到频道「"+K+"」,可选:"+(F||"(频道列表为空)")}}B.push(H.slug)}}const _={title:X.title,text:X.content,tags:L};if(B.length>0)_.channel=B;if(W.cover)_.cover=String(W.cover);const U=await fetch("https://segmentfault.com/gateway/article",{method:"POST",credentials:"include",headers:N,body:JSON.stringify(_)}),T=await U.text();if(T==="Unauthorized"||T.includes("禁言")||T.includes("锁定"))return{ok:!1,stage:"publish",status:U.status,message:T==="Unauthorized"?"未授权,请检查登录态":T};let V;try{V=JSON.parse(T)}catch(Q){return{ok:!1,stage:"publish",status:U.status,message:"解析发布响应失败:"+T.slice(0,200)}}if(V&&(V.isError||V.error||V.errno||V.scene_id)){let Q=V.message||V.msg||V.error||"";if(V.scene_id)Q=(Q?Q+";":"")+"思否要求人机验证(geetest),无法在自动化环境完成";return{ok:!1,stage:"publish",status:U.status,message:Q||"发布失败:"+T.slice(0,200)}}const E=V&&V.data&&V.data.id;if(E==null)return{ok:!1,stage:"publish",status:U.status,message:V&&(V.message||V.msg)||"发布响应缺少文章 id:"+T.slice(0,200)};return{ok:!0,draft:!1,id:String(E),url:"https://segmentfault.com/a/"+E}}};y({site:"segmentfault",name:"article",access:"write",description:"发布文章到思否(SegmentFault)。默认正式发布,加 --draft 仅存草稿。正文默认 Markdown;外链图片自动转存到思否图床。正式发布必须指定标签(--tags),合法标签用 `segmentfault tags <名称>` 校验、频道用 `segmentfault channels` 列举。",domain:"segmentfault.com",strategy:C.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(选填)。需先把图片上传到思否图床得到 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(X,A)=>{if(!X)throw new v("思否发布需要浏览器会话");I(A);const W=String(A.title??"").trim();if(!W)throw new O("INVALID_INPUT","文章标题不能为空");const G=await h(A),Z=Boolean(A.draft),M=(_)=>typeof _==="string"?_.split(",").map((U)=>U.trim()).filter(Boolean):[],b=M(A.tags),R=M(A.channels),N=typeof A.cover==="string"?A.cover.trim():"";if(!Z&&b.length===0)throw new O("INVALID_INPUT","思否正式发布必须指定 --tags(至少 1 个标签);合法标签用 `segmentfault tags <名称>` 校验,或加 --draft 只存草稿");const j=await p(X,{title:W,body:G,format:A.html?"html":"markdown",draftOnly:Z,profile:segmentfaultProfile,publishParams:{tags:b,channels:R,cover:N}}),L=j.images.uploaded.length|0,$=j.images.failed.length|0;let B=j.draft?"已保存到思否草稿箱(需在网页上手动发布)":"已正式发布到思否";if(L||$)B+=`;图片:${L} 张转存成功${$?`,${$} 张失败`:""}`;return w(B,"article","",j.draft?"draft":"created",{created_target:(j.draft?"draft:":"article:")+j.id,created_url:j.url})}});
@@ -1 +1 @@
1
- import{cli as B,Strategy as D}from"@jackwener/opencli/registry";import{CommandExecutionError as z}from"@jackwener/opencli/errors";import{gotoWritePage as F}from"../_shared/article/publish.js";import{segmentfaultProfile as G}from"./article.js";B({site:"segmentfault",name:"channels",access:"read",description:"列出思否文章频道(id + 名称),供 `segmentfault article --channels` 取合法频道名。",domain:"segmentfault.com",strategy:D.COOKIE,browser:!0,columns:["channel_id","channel_name"],func:async(v)=>{if(!v)throw new z("思否频道列举需要浏览器会话");await F(v,G.home);const A='(async () => {const tr = await fetch("https://segmentfault.com/write", { credentials: "include" });const html = await tr.text();let token = null;const m = html.match(/serverData"\\s*:\\s*\\{\\s*"Token"\\s*:\\s*"([^"]+)"/);if (m) { token = m[1]; } else { const mark = "window.g_initialProps = "; const ai = html.indexOf(mark); if (ai !== -1) { const ei = html.indexOf(";\\n\\t</script>", ai); if (ei !== -1) { try { const c = JSON.parse(html.substring(ai + mark.length, ei)); token = c && c.global && c.global.sessionInfo && c.global.sessionInfo.key; } catch (e) {} } }}'+'if (!token) return { __error: "获取思否 session token 失败,请确认已登录" };'+'const r = await fetch("https://segmentfault.com/gateway/channels", { credentials: "include", headers: { token: token, accept: "*/*" } });const t = await r.text();'+'let d; try { d = JSON.parse(t); } catch (e) { return { __error: "解析频道响应失败:" + t.slice(0, 200) }; }'+'if (!r.ok || typeof d === "string") return { __error: "思否频道接口请求被拒(HTTP " + r.status + "):" + (typeof d === "string" ? d : t.slice(0, 120)) + "。思否 GET /gateway 需客户端 URL 签名(getUrl/sign),当前未实现。" };'+'let list = [];if (Array.isArray(d)) list = d;else if (d && Array.isArray(d.data)) list = d.data;else if (d && Array.isArray(d.rows)) list = d.rows;else if (d && Array.isArray(d.channels)) list = d.channels;return list.map((c) => ({ channel_id: String((c && c.id) != null ? c.id : ""), channel_name: String((c && (c.name || c.title || c.text)) || "") }));})()',q=await v.evaluate(A);if(q&&q.__error)throw new z(q.__error);return Array.isArray(q)?q:[]}});
1
+ import{cli as H,Strategy as K}from"@jackwener/opencli/registry";import{CommandExecutionError as A}from"@jackwener/opencli/errors";import{gotoWritePage as L}from"../_shared/article/publish.js";import{segmentfaultProfile as M}from"./article.js";import{SF_SIGNED_GET_JS as O}from"./_signed-request.js";H({site:"segmentfault",name:"channels",access:"read",description:"列出思否文章频道(名称 + slug),供 `segmentfault article --channels` 取合法频道名。",domain:"segmentfault.com",strategy:K.COOKIE,browser:!0,columns:["channel_slug","channel_name"],func:async(z)=>{if(!z)throw new A("思否频道列举需要浏览器会话");await L(z,M.home);const B="(async () => {"+O+'const res = await __sfSignedGet("/channels");if (res && res.__sfErr) return { __error: res.__sfErr };'+'let d; try { d = JSON.parse(res.raw); } catch (e) { return { __error: "解析频道响应失败(HTTP " + res.status + "):" + String(res.raw).slice(0, 200) }; }'+'if (res.status !== 200 || typeof d === "string") return { __error: "思否频道接口请求被拒(HTTP " + res.status + "):" + (typeof d === "string" ? d : String(res.raw).slice(0, 120)) };'+'let list = [];if (Array.isArray(d)) list = d;else if (d && Array.isArray(d.rows)) list = d.rows;else if (d && Array.isArray(d.data)) list = d.data;else if (d && Array.isArray(d.channels)) list = d.channels;return list.map((c) => ({ channel_slug: String((c && c.slug) || ""), channel_name: String((c && (c.name || c.title || c.text)) || "") }));})()',v=await z.evaluate(B);if(v&&v.__error)throw new A(v.__error);return Array.isArray(v)?v:[]}});
@@ -1 +1 @@
1
- import{cli as F,Strategy as G}from"@jackwener/opencli/registry";import{CliError as H,CommandExecutionError as A}from"@jackwener/opencli/errors";import{gotoWritePage as I}from"../_shared/article/publish.js";import{segmentfaultProfile as J}from"./article.js";F({site:"segmentfault",name:"tags",access:"read",description:"按名称校验思否标签是否存在并返回其真实 id,供 `segmentfault article --tags` 取合法标签。思否发文强制选标签,禁止臆造标签名/id。",domain:"segmentfault.com",strategy:G.COOKIE,browser:!0,args:[{name:"name",positional:!0,required:!0,help:"要校验的标签名(精确匹配,如 JavaScript / Vue.js)"}],columns:["exists","tag_id","tag_name"],func:async(v,B)=>{if(!v)throw new A("思否标签校验需要浏览器会话");const z=String(B.name??"").trim();if(!z)throw new H("INVALID_INPUT","标签名不能为空");await I(v,J.home);const D='(async () => {const tr = await fetch("https://segmentfault.com/write", { credentials: "include" });const html = await tr.text();let token = null;const m = html.match(/serverData"\\s*:\\s*\\{\\s*"Token"\\s*:\\s*"([^"]+)"/);if (m) { token = m[1]; } else { const mark = "window.g_initialProps = "; const ai = html.indexOf(mark); if (ai !== -1) { const ei = html.indexOf(";\\n\\t</script>", ai); if (ei !== -1) { try { const c = JSON.parse(html.substring(ai + mark.length, ei)); token = c && c.global && c.global.sessionInfo && c.global.sessionInfo.key; } catch (e) {} } }}'+'if (!token) return { __error: "获取思否 session token 失败,请确认已登录" };'+"const name = "+JSON.stringify(z)+';const r = await fetch("https://segmentfault.com/gateway/tag/" + encodeURIComponent(name), { credentials: "include", headers: { token: token, Authorization: "Bearer " + token, accept: "*/*" } });const t = await r.text();'+'let d; try { d = JSON.parse(t); } catch (e) { return { __error: "解析标签响应失败:" + t.slice(0, 200) }; }'+'if (!r.ok || typeof d === "string") return { __error: "思否标签接口请求被拒(HTTP " + r.status + "):" + (typeof d === "string" ? d : t.slice(0, 120)) + "。思否 GET /gateway 需客户端 URL 签名(getUrl/sign),当前未实现,无法校验标签。" };'+'const tag = d && d.tag;if (!tag || tag.id == null) return [{ exists: false, tag_id: "", tag_name: name }];return [{ exists: true, tag_id: String(tag.id), tag_name: String(tag.name || name) }];})()',q=await v.evaluate(D);if(q&&q.__error)throw new A(q.__error);return Array.isArray(q)?q:[]}});
1
+ import{cli as L,Strategy as M}from"@jackwener/opencli/registry";import{CliError as O,CommandExecutionError as B}from"@jackwener/opencli/errors";import{gotoWritePage as Q}from"../_shared/article/publish.js";import{segmentfaultProfile as R}from"./article.js";import{SF_SIGNED_GET_JS as U}from"./_signed-request.js";L({site:"segmentfault",name:"tags",access:"read",description:"按名称校验思否标签是否存在并返回其真实 id,供 `segmentfault article --tags` 取合法标签。思否发文强制选标签,禁止臆造标签名/id。",domain:"segmentfault.com",strategy:M.COOKIE,browser:!0,args:[{name:"name",positional:!0,required:!0,help:"要校验的标签名(精确匹配,如 JavaScript / Vue.js)"}],columns:["exists","tag_id","tag_name"],func:async(z,H)=>{if(!z)throw new B("思否标签校验需要浏览器会话");const A=String(H.name??"").trim();if(!A)throw new O("INVALID_INPUT","标签名不能为空");await Q(z,R.home);const K="(async () => {"+U+"const name = "+JSON.stringify(A)+';const res = await __sfSignedGet("/tag/" + encodeURIComponent(name));if (res && res.__sfErr) return { __error: res.__sfErr };'+'let d; try { d = JSON.parse(res.raw); } catch (e) { return { __error: "解析标签响应失败(HTTP " + res.status + "):" + String(res.raw).slice(0, 200) }; }'+'if (res.status !== 200 || typeof d === "string") return { __error: "思否标签接口请求被拒(HTTP " + res.status + "):" + (typeof d === "string" ? d : String(res.raw).slice(0, 120)) };'+'const tag = d && d.tag;if (!tag || tag.id == null) return [{ exists: false, tag_id: "", tag_name: name }];return [{ exists: true, tag_id: String(tag.id), tag_name: String(tag.name || name) }];})()',v=await z.evaluate(K);if(v&&v.__error)throw new B(v.__error);return Array.isArray(v)?v:[]}});
@@ -1,4 +1,4 @@
1
- import*as _ from"node:fs";import{cli as $,Strategy as M}from"@jackwener/opencli/registry";import{ArgumentError as Q,AuthRequiredError as W,CommandExecutionError as N}from"@jackwener/opencli/errors";const F="https://www.tumblr.com",j="https://www.tumblr.com/dashboard",X=["published","draft","queue","private"];export function blogHost(z){const G=String(z||"").trim();if(!G)return"";return G.includes(".")?G:`${G}.tumblr.com`}export function resolveBody(z){if(z["body-file"]){const G=String(z["body-file"]);if(!_.statSync(G,{throwIfNoEntry:!1})?.isFile())throw new Q(`--body-file not found: ${G}`);return _.readFileSync(G,"utf8")}if(z.body!==void 0&&z.body!==null)return String(z.body);if(z.text!==void 0&&z.text!==null)return String(z.text);return}export function buildContent(z,G){const I=[];if(z.title)I.push({type:"text",subtype:"heading1",text:String(z.title)});if(G!==void 0&&G!=="")for(const L of String(G).split(/\n{2,}/)){const J=L.replace(/\n+$/,"");if(J.trim()!=="")I.push({type:"text",text:J})}if(z["image-url"])I.push({type:"image",media:[{url:String(z["image-url"])}]});if(z.link)I.push({type:"link",url:String(z.link)});if(I.length===0)throw new Q("Provide at least one of --title, --text/--body/--body-file, --image-url, or --link");return I}export function resolveTags(z){if(z===void 0||z===null||z==="")return"";return String(z).split(",").map((G)=>G.trim()).filter(Boolean).join(",")}async function Y(z,G,I){return z.evaluate(`(async () => {
1
+ import*as M from"node:fs";import{cli as _,Strategy as L}from"@jackwener/opencli/registry";import{ArgumentError as U,AuthRequiredError as X,CommandExecutionError as P}from"@jackwener/opencli/errors";import{assertLiteralContent as j}from"../_shared/content-guard.js";const O="https://www.tumblr.com",q="https://www.tumblr.com/dashboard",Y=["published","draft","queue","private"];export function blogHost(z){const G=String(z||"").trim();if(!G)return"";return G.includes(".")?G:`${G}.tumblr.com`}export function resolveBody(z){if(z["body-file"]){const G=String(z["body-file"]);if(!M.statSync(G,{throwIfNoEntry:!1})?.isFile())throw new U(`--body-file not found: ${G}`);return M.readFileSync(G,"utf8")}if(z.body!==void 0&&z.body!==null){j(String(z.body),{fileFlag:"--body-file"});return String(z.body)}if(z.text!==void 0&&z.text!==null){j(String(z.text),{fileFlag:"--body-file"});return String(z.text)}return}export function buildContent(z,G){const I=[];if(z.title)I.push({type:"text",subtype:"heading1",text:String(z.title)});if(G!==void 0&&G!=="")for(const N of String(G).split(/\n{2,}/)){const J=N.replace(/\n+$/,"");if(J.trim()!=="")I.push({type:"text",text:J})}if(z["image-url"])I.push({type:"image",media:[{url:String(z["image-url"])}]});if(z.link)I.push({type:"link",url:String(z.link)});if(I.length===0)throw new U("Provide at least one of --title, --text/--body/--body-file, --image-url, or --link");return I}export function resolveTags(z){if(z===void 0||z===null||z==="")return"";return String(z).split(",").map((G)=>G.trim()).filter(Boolean).join(",")}async function Z(z,G,I){return z.evaluate(`(async () => {
2
2
  try {
3
3
  if (!(window.tumblr && typeof window.tumblr.apiFetch === 'function')) {
4
4
  return { kind: 'auth', detail: 'apiFetch unavailable — not on a logged-in tumblr page?' };
@@ -11,4 +11,4 @@ import*as _ from"node:fs";import{cli as $,Strategy as M}from"@jackwener/opencli/
11
11
  if (status === 401 || status === 403) return { kind: 'auth', detail: 'HTTP ' + status + ' ' + msg };
12
12
  return { kind: 'http', status: status, detail: String(msg).slice(0, 300) };
13
13
  }
14
- })()`)}async function q(z,G){if(G.blog)return blogHost(G.blog);const I=await Y(z,"/v2/user/info",{method:"GET"});if(I?.kind==="auth")throw new W("tumblr.com",`Not logged in (${I.detail}). Run \`opencli tumblr login\`.`);if(I?.kind!=="ok"||!I.response||!I.response.user)throw new N(`Failed to resolve default blog: ${JSON.stringify(I)}`);const L=I.response.user.blogs||[],J=L.find((P)=>P.primary)||L[0];if(!J||!J.name)throw new N("No blog found on this account; pass --blog explicitly.");return blogHost(J.name)}$({site:"tumblr",name:"publish",access:"write",description:"Publish a Tumblr post (NPF: title/text/image/link). Draft by default; pass --state published to go live.",domain:"tumblr.com",strategy:M.COOKIE,browser:!0,args:[{name:"title",type:"string",required:!1,help:"Title (rendered as a heading1 text block)"},{name:"text",type:"string",required:!1,help:"Body text (blank lines split paragraphs); alias of --body"},{name:"body",type:"string",required:!1,help:"Body text (blank lines split paragraphs)"},{name:"body-file",type:"string",required:!1,help:"Path to a text file for the body"},{name:"image-url",type:"string",required:!1,help:"Image URL (NPF image block)"},{name:"link",type:"string",required:!1,help:"Link URL (NPF link block)"},{name:"tags",type:"string",required:!1,help:"Comma-separated tags"},{name:"state",type:"string",required:!1,default:"draft",choices:X,help:"draft | published | queue | private (default: draft)"},{name:"blog",type:"string",required:!1,help:"Target blog identifier (else your primary blog)"}],columns:["status","id","blog","state","url"],func:async(z,G)=>{if(!z)throw new N("Browser session required for tumblr publish");const I=String(G.state??"draft");if(!X.includes(I))throw new Q(`Invalid --state "${I}". One of: ${X.join(", ")}`);const L=resolveBody(G),J=buildContent(G,L),P=resolveTags(G.tags);await z.goto(j);const U=await q(z,G),Z={content:J,state:I};if(P)Z.tags=P;const K=await Y(z,`/v2/blog/${U}/posts`,{method:"POST",body:Z});if(K?.kind==="auth")throw new W("tumblr.com",`Not logged in (${K.detail}). Run \`opencli tumblr login\`.`);if(K?.kind==="http")throw new N(`Tumblr publish failed: HTTP ${K.status} ${K.detail}`);if(K?.kind!=="ok"||!K.response)throw new N(`Unexpected Tumblr response: ${JSON.stringify(K)}`);const V=K.response.id_string??(K.response.id!=null?String(K.response.id):"");if(!V)throw new N("Tumblr post response missing id");return[{status:"success",id:V,blog:U,state:I,url:I==="published"?`${F}/${U}/${V}`:""}]}});$({site:"tumblr",name:"delete",access:"write",description:"Delete a Tumblr post by id (POST /v2/blog/{blog}/post/delete).",domain:"tumblr.com",strategy:M.COOKIE,browser:!0,args:[{name:"id",type:"string",required:!0,help:"Post id to delete"},{name:"blog",type:"string",required:!1,help:"Target blog identifier (else your primary blog)"}],columns:["status","id","blog"],func:async(z,G)=>{if(!z)throw new N("Browser session required for tumblr delete");const I=String(G.id??"").trim();if(!I)throw new Q("--id is required");await z.goto(j);const L=await q(z,G),J=await Y(z,`/v2/blog/${L}/post/delete`,{method:"POST",body:{id:I}});if(J?.kind==="auth")throw new W("tumblr.com",`Not logged in (${J.detail}). Run \`opencli tumblr login\`.`);if(J?.kind==="http")throw new N(`Tumblr delete failed: HTTP ${J.status} ${J.detail}`);if(J?.kind!=="ok")throw new N(`Unexpected Tumblr delete response: ${JSON.stringify(J)}`);return[{status:"deleted",id:I,blog:L}]}});export const __test__={blogHost,resolveBody,buildContent,resolveTags};
14
+ })()`)}async function F(z,G){if(G.blog)return blogHost(G.blog);const I=await Z(z,"/v2/user/info",{method:"GET"});if(I?.kind==="auth")throw new X("tumblr.com",`Not logged in (${I.detail}). Run \`opencli tumblr login\`.`);if(I?.kind!=="ok"||!I.response||!I.response.user)throw new P(`Failed to resolve default blog: ${JSON.stringify(I)}`);const N=I.response.user.blogs||[],J=N.find((Q)=>Q.primary)||N[0];if(!J||!J.name)throw new P("No blog found on this account; pass --blog explicitly.");return blogHost(J.name)}_({site:"tumblr",name:"publish",access:"write",description:"Publish a Tumblr post (NPF: title/text/image/link). Draft by default; pass --state published to go live.",domain:"tumblr.com",strategy:L.COOKIE,browser:!0,args:[{name:"title",type:"string",required:!1,help:"Title (rendered as a heading1 text block)"},{name:"text",type:"string",required:!1,help:"Body text (blank lines split paragraphs); alias of --body"},{name:"body",type:"string",required:!1,help:"Body text (blank lines split paragraphs)"},{name:"body-file",type:"string",required:!1,help:"Path to a text file for the body"},{name:"image-url",type:"string",required:!1,help:"Image URL (NPF image block)"},{name:"link",type:"string",required:!1,help:"Link URL (NPF link block)"},{name:"tags",type:"string",required:!1,help:"Comma-separated tags"},{name:"state",type:"string",required:!1,default:"draft",choices:Y,help:"draft | published | queue | private (default: draft)"},{name:"blog",type:"string",required:!1,help:"Target blog identifier (else your primary blog)"}],columns:["status","id","blog","state","url"],func:async(z,G)=>{if(!z)throw new P("Browser session required for tumblr publish");const I=String(G.state??"draft");if(!Y.includes(I))throw new U(`Invalid --state "${I}". One of: ${Y.join(", ")}`);const N=resolveBody(G),J=buildContent(G,N),Q=resolveTags(G.tags);await z.goto(q);const V=await F(z,G),$={content:J,state:I};if(Q)$.tags=Q;const K=await Z(z,`/v2/blog/${V}/posts`,{method:"POST",body:$});if(K?.kind==="auth")throw new X("tumblr.com",`Not logged in (${K.detail}). Run \`opencli tumblr login\`.`);if(K?.kind==="http")throw new P(`Tumblr publish failed: HTTP ${K.status} ${K.detail}`);if(K?.kind!=="ok"||!K.response)throw new P(`Unexpected Tumblr response: ${JSON.stringify(K)}`);const W=K.response.id_string??(K.response.id!=null?String(K.response.id):"");if(!W)throw new P("Tumblr post response missing id");return[{status:"success",id:W,blog:V,state:I,url:I==="published"?`${O}/${V}/${W}`:""}]}});_({site:"tumblr",name:"delete",access:"write",description:"Delete a Tumblr post by id (POST /v2/blog/{blog}/post/delete).",domain:"tumblr.com",strategy:L.COOKIE,browser:!0,args:[{name:"id",type:"string",required:!0,help:"Post id to delete"},{name:"blog",type:"string",required:!1,help:"Target blog identifier (else your primary blog)"}],columns:["status","id","blog"],func:async(z,G)=>{if(!z)throw new P("Browser session required for tumblr delete");const I=String(G.id??"").trim();if(!I)throw new U("--id is required");await z.goto(q);const N=await F(z,G),J=await Z(z,`/v2/blog/${N}/post/delete`,{method:"POST",body:{id:I}});if(J?.kind==="auth")throw new X("tumblr.com",`Not logged in (${J.detail}). Run \`opencli tumblr login\`.`);if(J?.kind==="http")throw new P(`Tumblr delete failed: HTTP ${J.status} ${J.detail}`);if(J?.kind!=="ok")throw new P(`Unexpected Tumblr delete response: ${JSON.stringify(J)}`);return[{status:"deleted",id:I,blog:N}]}});export const __test__={blogHost,resolveBody,buildContent,resolveTags};
@@ -1,7 +1,7 @@
1
- import{cli as Z,Strategy as $}from"@jackwener/opencli/registry";import{ArgumentError as q,CommandExecutionError as V}from"@jackwener/opencli/errors";import{DM_SEND_QUERY as N,buildQuery as P,getDmAuth as R,resolveTwitterUserId as T,shapeDmMessages as Y}from"./dm-shared.js";Z({site:"twitter",name:"dm-send",access:"write",description:"Send a direct message to one user (by @screen_name, numeric user id, or conversation id from dm-list)",domain:"x.com",strategy:$.COOKIE,browser:!0,args:[{name:"user",type:"string",required:!0,positional:!0,help:"Recipient: @screen_name, numeric user id, or a conversation id like 123-456 from `dm-list`"},{name:"text",type:"string",required:!0,positional:!0,help:"Message text to send"},{name:"reply-to",type:"string",required:!1,help:"Message id to reply to (from `dm-read`)"}],columns:["id","conversation_id","text","time","status"],func:async(G,B)=>{const J=String(B.text??"").trim();if(!J)throw new q("Message text must not be empty");const{ownId:W,headers:K}=await R(G),H=String(B.user??"").trim();let F;if(/^\d+-\d+$/.test(H))F=H;else F=`${await T(G,H,K)}-${W}`;const L={cards_platform:"Web-12",conversation_id:F,dm_users:!1,include_cards:1,include_quote_count:!0,recipient_ids:!1,text:J,audio_only_media_attachment:!1};if(B["reply-to"])L.reply_to_dm_id=String(B["reply-to"]).trim();const X="https://x.com/i/api/1.1/dm/new2.json?"+P(N),f=await G.evaluate(`async () => {
2
- const body = ${JSON.stringify(L)};
1
+ import{cli as $,Strategy as q}from"@jackwener/opencli/registry";import{ArgumentError as L,CommandExecutionError as W}from"@jackwener/opencli/errors";import{DM_SEND_QUERY as N,buildQuery as P,getDmAuth as R,resolveTwitterUserId as T,shapeDmMessages as Y}from"./dm-shared.js";import{assertLiteralContent as A}from"../_shared/content-guard.js";$({site:"twitter",name:"dm-send",access:"write",description:"Send a direct message to one user (by @screen_name, numeric user id, or conversation id from dm-list)",domain:"x.com",strategy:q.COOKIE,browser:!0,args:[{name:"user",type:"string",required:!0,positional:!0,help:"Recipient: @screen_name, numeric user id, or a conversation id like 123-456 from `dm-list`"},{name:"text",type:"string",required:!0,positional:!0,help:"Message text to send"},{name:"reply-to",type:"string",required:!1,help:"Message id to reply to (from `dm-read`)"}],columns:["id","conversation_id","text","time","status"],func:async(G,B)=>{const H=String(B.text??"").trim();A(H,{label:"私信内容"});if(!H)throw new L("Message text must not be empty");const{ownId:X,headers:K}=await R(G),J=String(B.user??"").trim();let F;if(/^\d+-\d+$/.test(J))F=J;else F=`${await T(G,J,K)}-${X}`;const O={cards_platform:"Web-12",conversation_id:F,dm_users:!1,include_cards:1,include_quote_count:!0,recipient_ids:!1,text:H,audio_only_media_attachment:!1};if(B["reply-to"])O.reply_to_dm_id=String(B["reply-to"]).trim();const Z="https://x.com/i/api/1.1/dm/new2.json?"+P(N),f=await G.evaluate(`async () => {
2
+ const body = ${JSON.stringify(O)};
3
3
  body.request_id = crypto.randomUUID();
4
- const r = await fetch(${JSON.stringify(X)}, {
4
+ const r = await fetch(${JSON.stringify(Z)}, {
5
5
  method: 'POST',
6
6
  headers: Object.assign(${JSON.stringify(K)}, { 'Content-Type': 'application/json' }),
7
7
  credentials: 'include',
@@ -10,4 +10,4 @@ import{cli as Z,Strategy as $}from"@jackwener/opencli/registry";import{ArgumentE
10
10
  const text = await r.text();
11
11
  if (!r.ok) return { error: r.status, body: text.slice(0, 300) };
12
12
  try { return JSON.parse(text); } catch { return { error: 'bad-json', body: text.slice(0, 300) }; }
13
- }`);if(f?.error)throw new V(`dm/new2.json failed: HTTP ${f.error} ${f.body||""}`);const O=Y(f.entries,f.users);if(!O.length)throw new V(`DM send returned no message entry (response keys: ${Object.keys(f||{}).join(",")})`);return O.map((z)=>({id:z.id,conversation_id:z.conversation_id||F,text:z.text,time:z.time,status:"sent"}))}});
13
+ }`);if(f?.error)throw new W(`dm/new2.json failed: HTTP ${f.error} ${f.body||""}`);const V=Y(f.entries,f.users);if(!V.length)throw new W(`DM send returned no message entry (response keys: ${Object.keys(f||{}).join(",")})`);return V.map((z)=>({id:z.id,conversation_id:z.conversation_id||F,text:z.text,time:z.time,status:"sent"}))}});