publishport-opencli 1.8.5-pp.22 → 1.8.5-pp.24

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.
@@ -1,22 +1,22 @@
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(`(() => {
1
+ import{CommandExecutionError as O}from"@jackwener/opencli/errors";import{normalizeContent as H}from"./format.js";import{assertLiteralContent as C}from"../content-guard.js";import{inlineLocalImages as U,isLocalImagePath as I,localImageToDataUri as E}from"./images.js";import{PAGE_RUNTIME as S}from"./page-runtime.js";export function selectContent(K,Q,X="auto"){const Z=H(K,{format:X});return Q.outputFormat==="markdown"?Z.markdown:Z.html}export function originReFromHome(K){let Q="";try{Q=new URL(K).host}catch(Z){Q=""}return"^https?://([^/]*\\.)?"+Q.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")+"(/|$)"}export async function gotoWritePage(K,Q,X){const Z=X||originReFromHome(Q);for(let G=0;G<8;G++){await K.goto(Q);await K.wait({time:1});const D=await K.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
- })()`);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
- `+E+`
8
- const I = `+JSON.stringify(B)+`;
9
- const __publish = (`+K+`);
10
- `+(V?"const __upload = ("+V+`);
6
+ })()`);if(D&&D.onSite&&D.cookieOk)return;await K.wait({time:1})}throw new O(`无法在浏览器中打开已登录的写作页(页面停留在空白页,未落到 ${Q})。`+"请确认登录该平台的 Chrome 已连接 opencli 浏览器桥。")}export function buildPublishJs(K,Q,X){return`(async () => {
7
+ `+S+`
8
+ const I = `+JSON.stringify(K)+`;
9
+ const __publish = (`+Q+`);
10
+ `+(X?"const __upload = ("+X+`);
11
11
  `:"")+`const __content0 = I.content;
12
12
  let content = I.content;
13
13
  if (I.outputFormat === "html" && I.preprocessConfig) content = PP.preprocess(content, I.preprocessConfig);
14
- `+(V?`const __t = await PP.processImagesWith(content, (src) => __upload(src, PP), { skip: I.imageSkip });
14
+ `+(X?`const __t = await PP.processImagesWith(content, (src) => __upload(src, PP), { skip: I.imageSkip });
15
15
  `:`const __t = await PP.transferImages(content, I.imageSpec, I.imageSkip);
16
16
  `)+`content = __t.content;
17
17
  if (I.publishParams && typeof I.publishParams.cover === "string" && I.publishParams.cover) {
18
18
  var __cvWrap = '<img src="' + I.publishParams.cover + '">';
19
- `+(V?` var __cv = await PP.processImagesWith(__cvWrap, (src) => __upload(src, PP), { skip: I.imageSkip });
19
+ `+(X?` var __cv = await PP.processImagesWith(__cvWrap, (src) => __upload(src, PP), { skip: I.imageSkip });
20
20
  `:` var __cv = await PP.transferImages(__cvWrap, I.imageSpec, I.imageSkip);
21
21
  `)+` if (__cv.failed.length) {
22
22
  `+` return { ok: false, stage: "cover", message: "封面图转存失败:" + __cv.failed[0].error, uploaded: __t.uploaded, failed: __t.failed.concat(__cv.failed) };
@@ -28,7 +28,7 @@ const __pub = await __publish({ title: I.title, content: content, markdown: I.ma
28
28
  var __upN = (__t.uploaded || []).concat((__pub && __pub.uploaded) || []);
29
29
  var __failN = (__t.failed || []).concat((__pub && __pub.failed) || []);
30
30
  if (!__pub || __pub.ok === false) {
31
- return { ok: false, stage: (__pub && __pub.stage) || "publish", status: __pub && __pub.status, message: (__pub && __pub.message) || "publish failed", uploaded: __upN, failed: __failN };
31
+ return { ok: false, stage: (__pub && __pub.stage) || "publish", status: __pub && __pub.status, message: (__pub && __pub.message) || "publish failed", detail: __pub && __pub.detail, uploaded: __upN, failed: __failN };
32
32
  }
33
33
  return { ok: true, id: String(__pub.id == null ? "" : __pub.id), url: __pub.url || "", draft: !!__pub.draft, uploaded: __upN, failed: __failN };
34
- })()`}export async function publishArticle(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};
34
+ })()`}export async function publishArticle(K,Q){const{title:X,body:Z,format:G="auto",draftOnly:D=!1,profile:q,publishParams:k=null}=Q;if(!q)throw Error("publishArticle: profile is required");if(!q.home)throw Error("publishArticle: profile.home is required");if(typeof q.publish!=="function")throw Error("publishArticle: profile.publish must be a function");C(Z,{fileFlag:"--file"});const W=H(Z,{format:G}),w=q.outputFormat==="markdown"||q.needsMarkdown===!0,L=q.outputFormat==="html"||q.needsHtml===!0,_=w?await U(W.markdown):{content:null,missing:[]},z=L?await U(W.html):{content:null,missing:[]},A=_.content,J=z.content,y=q.outputFormat==="markdown"?A:J,N=[],T=new Set;for(const V of[..._.missing,...z.missing]){if(T.has(V.src))continue;T.add(V.src);N.push(V)}let Y=k;if(Y&&typeof Y.cover==="string"&&Y.cover){const V=Y.cover.trim();if(/["<>\s]/.test(V))throw new O("封面图路径/URL 含非法字符(引号/尖括号/空白):"+V.slice(0,120));if(I(V))try{const{dataUri:$}=await E(V);Y={...Y,cover:$}}catch($){throw new O("封面图读取失败:"+V+"("+String($&&$.message||$)+")")}else Y={...Y,cover:V}}await gotoWritePage(K,q.home,q.originRe);const R={title:X,content:y,markdown:q.needsMarkdown===!0&&q.outputFormat!=="markdown"?A:null,html:q.needsHtml===!0&&q.outputFormat!=="html"?J:null,draftOnly:!!D,outputFormat:q.outputFormat,preprocessConfig:q.preprocessConfig||null,imageSpec:q.image?.spec||null,imageSkip:q.image?.skip||[],publishParams:Y},j=typeof q.image?.uploadFn==="function"?q.image.uploadFn.toString():null,x=buildPublishJs(R,q.publish.toString(),j),B=await K.evaluate(x);if(!B||B.ok===!1){const V=B?.stage||"publish",$=B?.status!=null?` (HTTP ${B.status})`:"",M=B?.detail!=null?`;接口原始响应:${typeof B.detail==="string"?B.detail:JSON.stringify(B.detail)}`:"";throw new O(`[${V}] ${B?.message||"发布失败"}${$}${M}`)}return{id:B.id,url:B.url,draft:B.draft,images:{uploaded:B.uploaded||[],failed:(B.failed||[]).concat(N)}}}export const __test__={selectContent,originReFromHome,buildPublishJs,publishArticle};
package/clis/jike/auth.js CHANGED
@@ -1,4 +1,4 @@
1
- import{AuthRequiredError as N,CommandExecutionError as L}from"@jackwener/opencli/errors";import{registerSiteAuthCommands as X}from"../_shared/site-auth.js";const V=`(async () => {
1
+ import{AuthRequiredError as L,CommandExecutionError as K}from"@jackwener/opencli/errors";import{registerSiteAuthCommands as Q}from"../_shared/site-auth.js";const N=`(async () => {
2
2
  try {
3
3
  const token = localStorage.getItem('JK_ACCESS_TOKEN') || '';
4
4
  if (!token) return { kind: 'auth', detail: 'Jike JK_ACCESS_TOKEN missing from localStorage (anonymous)' };
@@ -14,4 +14,6 @@ import{AuthRequiredError as N,CommandExecutionError as L}from"@jackwener/opencli
14
14
  } catch (e) {
15
15
  return { kind: 'exception', detail: String(e && e.message || e) };
16
16
  }
17
- })()`;async function Y(F){await F.goto("https://web.okjike.com/");await F.wait(2);let G="";for(let K=0;K<30;K++){G=await F.evaluate("() => location.href");if(G.includes("okjike.com"))break;await F.wait(0.5)}if(!G.includes("okjike.com")){await F.screenshot({path:"/tmp/jike_whoami_nav_debug.png"});throw new L(`Jike whoami: navigation never settled on okjike.com (landed on ${G}). Debug screenshot: /tmp/jike_whoami_nav_debug.png`)}let z=await F.evaluate(V);for(let K=0;K<30&&z?.kind==="auth";K++){await F.wait(0.5);z=await F.evaluate(V)}if(z?.kind==="auth")throw new N("web.okjike.com",z.detail);if(z?.kind==="http")throw new L(`HTTP ${z.httpStatus} from Jike users/profile`);if(z?.kind==="exception"){const K=await F.evaluate("() => location.href");if(/^data:/.test(K)||/\/login/.test(G))throw new N("web.okjike.com","Jike session anonymous (redirected to /login)");throw new L(`Jike whoami failed: ${z.detail}`)}if(!z?.ok)throw new L(`Unexpected Jike probe: ${JSON.stringify(z)}`);return{user_id:z.user_id,screen_name:z.screen_name,username:z.username}}X({site:"jike",domain:"web.okjike.com",loginUrl:"https://web.okjike.com/login",columns:["user_id","screen_name","username"],verify:Y,poll:async(F)=>{const G=await F.evaluate(V);if(!G?.ok)throw new N("web.okjike.com","Waiting for Jike login");return{user_id:G.user_id,screen_name:G.screen_name,username:G.username}}});
17
+ })()`;async function T(z){await z.goto("https://web.okjike.com/");await z.wait(2);let F="";for(let G=0;G<30;G++){F=await z.evaluate("() => location.href");if(F.includes("okjike.com"))break;await z.wait(0.5)}if(!F.includes("okjike.com")){await z.screenshot({path:"/tmp/jike_whoami_nav_debug.png"});throw new K(`Jike whoami: navigation never settled on okjike.com (landed on ${F}). Debug screenshot: /tmp/jike_whoami_nav_debug.png`)}let D=await z.evaluate(N);for(let G=0;G<30&&D?.kind==="auth";G++){await z.wait(0.5);D=await z.evaluate(N)}if(D?.kind==="auth")throw new L("web.okjike.com",D.detail);if(D?.kind==="http")throw new K(`HTTP ${D.httpStatus} from Jike users/profile`);if(D?.kind==="exception"){const G=await z.evaluate("() => location.href");if(/^data:/.test(G)||/\/login/.test(F))throw new L("web.okjike.com","Jike session anonymous (redirected to /login)");throw new K(`Jike whoami failed: ${D.detail}`)}if(!D?.ok)throw new K(`Unexpected Jike probe: ${JSON.stringify(D)}`);return{user_id:D.user_id,screen_name:D.screen_name,username:D.username}}Q({site:"jike",domain:"web.okjike.com",loginUrl:"https://web.okjike.com/login",columns:["user_id","screen_name","username"],verify:T,quickCheck:async(z)=>{await z.goto("https://web.okjike.com/robots.txt");return{logged_in:await z.evaluate(`(() => {
18
+ try { return !!localStorage.getItem('JK_ACCESS_TOKEN'); } catch { return false; }
19
+ })()`)===!0}},poll:async(z)=>{const F=await z.evaluate(N);if(!F?.ok)throw new L("web.okjike.com","Waiting for Jike login");return{user_id:F.user_id,screen_name:F.screen_name,username:F.username}}});
package/clis/jike/feed.js CHANGED
@@ -1,5 +1,5 @@
1
- import{cli as v,Strategy as z}from"@jackwener/opencli/registry";import{getPostDataJs as A}from"./utils.js";v({site:"jike",name:"feed",access:"read",description:"即刻首页动态流",domain:"web.okjike.com",strategy:z.COOKIE,browser:!0,args:[{name:"limit",type:"int",default:20}],columns:["id","author","content","likes","comments","time","url"],func:async(b,q)=>{const d=q.limit||20;await b.goto("https://web.okjike.com");const h=async()=>{return await b.evaluate(`(() => {
2
- ${A}
1
+ import{cli as F,Strategy as G}from"@jackwener/opencli/registry";import{AuthRequiredError as H,EmptyResultError as I}from"@jackwener/opencli/errors";import{getPostDataJs as K}from"./utils.js";F({site:"jike",name:"feed",access:"read",description:"即刻首页动态流",domain:"web.okjike.com",strategy:G.COOKIE,browser:!0,args:[{name:"limit",type:"int",default:20}],columns:["id","author","content","likes","comments","time","url"],func:async(f,C)=>{const z=C.limit||20;await f.goto("https://web.okjike.com");const B=async()=>{return await f.evaluate(`(() => {
2
+ ${K}
3
3
 
4
4
  const results = [];
5
5
  const seen = new Set();
@@ -29,4 +29,4 @@ import{cli as v,Strategy as z}from"@jackwener/opencli/registry";import{getPostDa
29
29
  }
30
30
 
31
31
  return results;
32
- })()`)};let f=await h();if(f.length<d){await b.autoScroll({times:Math.ceil(d/10),delayMs:2000});f=await h()}return f.slice(0,d)}});
32
+ })()`)};let b=await B();for(let v=0;v<30&&b.length===0;v++){await f.wait(0.5);b=await B()}if(b.length===0){const v=await f.evaluate("() => location.href");if(/\/login/.test(v)||/^data:/.test(v))throw new H("web.okjike.com","Jike feed requires login (redirected to /login)");throw new I("jike feed","No posts rendered on web.okjike.com within 15s. The page structure may have changed.")}if(b.length<z){await f.autoScroll({times:Math.ceil(z/10),delayMs:2000});b=await B()}return b.slice(0,z)}});
package/clis/jike/like.js CHANGED
@@ -8,25 +8,21 @@ import{cli as m,Strategy as q}from"@jackwener/opencli/registry";m({site:"jike",n
8
8
  return { ok: false, message: '未找到点赞按钮' };
9
9
  }
10
10
 
11
- // 检查是否已点赞(已赞按钮带有 _liked_ 类)
12
- const cls = likeBtn.className || '';
13
- if (cls.includes('_liked')) {
11
+ // 已赞状态挂在内层 SVG 图标的类上(_likeIconLiked_),外层容器 div 的
12
+ // 类名点击前后不变——按容器类名判定会把成功点赞误报为失败(真机踩过)。
13
+ const isLiked = () => !!likeBtn.querySelector('[class*="_likeIconLiked_"]');
14
+ if (isLiked()) {
14
15
  return { ok: true, message: '该帖子已赞过' };
15
16
  }
16
17
 
17
- // 记录点击前的类名
18
- const beforeCls = likeBtn.className;
19
-
18
+ const beforeCount = (likeBtn.textContent || '').trim();
20
19
  likeBtn.click();
21
20
  await new Promise(r => setTimeout(r, 1500));
22
21
 
23
- // 验证:类名变化表示点赞成功
24
- const afterCls = likeBtn.className;
25
- if (afterCls !== beforeCls) {
22
+ // 验证:已赞图标出现或计数变化,任一即成功
23
+ if (isLiked() || (likeBtn.textContent || '').trim() !== beforeCount) {
26
24
  return { ok: true, message: '点赞成功' };
27
25
  }
28
-
29
- // 类名未变化,无法确认点赞是否成功
30
26
  return { ok: false, message: '点赞状态未确认,请手动检查' };
31
27
  } catch (e) {
32
28
  return { ok: false, message: e.toString() };
@@ -1,4 +1,4 @@
1
- import{cli as C,Strategy as D}from"@jackwener/opencli/registry";function E(d){if(!d)return"通知";const h=d.toUpperCase();if(h.includes("LIKE"))return"赞了你";if(h.includes("COMMENT"))return"评论了你";if(h.includes("FOLLOW"))return"关注了你";if(h.includes("REPOST"))return"转发了你";if(h.includes("MENTION"))return"提到了你";if(h.includes("REPLY"))return"回复了你";return d}C({site:"jike",name:"notifications",access:"read",description:"即刻通知",domain:"web.okjike.com",strategy:D.COOKIE,browser:!0,args:[{name:"limit",type:"int",default:20}],columns:["type","user","content","time"],func:async(d,h)=>{const z=h.limit||20;await d.goto("https://web.okjike.com/notification");const B=await d.evaluate(`(() => {
1
+ import{cli as D,Strategy as E}from"@jackwener/opencli/registry";function F(d){if(!d)return"通知";const h=d.toUpperCase();if(h.includes("LIKE"))return"赞了你";if(h.includes("COMMENT"))return"评论了你";if(h.includes("FOLLOW"))return"关注了你";if(h.includes("REPOST"))return"转发了你";if(h.includes("MENTION"))return"提到了你";if(h.includes("REPLY"))return"回复了你";return d}D({site:"jike",name:"notifications",access:"read",description:"即刻通知",domain:"web.okjike.com",strategy:E.COOKIE,browser:!0,args:[{name:"limit",type:"int",default:20}],columns:["type","user","content","time"],func:async(d,h)=>{const z=h.limit||20;await d.goto("https://web.okjike.com/notification");for(let B=0;B<30;B++){if(await d.evaluate(`(() => document.querySelectorAll('[class*="_item_"]').length > 0)()`))break;await d.wait(0.5)}const C=await d.evaluate(`(() => {
2
2
  // 从 React fiber 树中提取通知数据,向上最多走 15 层
3
3
  function getNotificationData(element) {
4
4
  for (const key of Object.keys(element)) {
@@ -57,7 +57,7 @@ import{cli as C,Strategy as D}from"@jackwener/opencli/registry";function E(d){if
57
57
  }
58
58
 
59
59
  return results;
60
- })()`);if(B.length>0)return B.map((q)=>({type:E(q.actionType),user:q.fromUser,content:q.content.replace(/\n/g," ").slice(0,100),time:q.time})).slice(0,z);await d.autoScroll({times:Math.ceil(z/10),delayMs:2000});return(await d.evaluate(`(() => {
60
+ })()`);if(C.length>0)return C.map((q)=>({type:F(q.actionType),user:q.fromUser,content:q.content.replace(/\n/g," ").slice(0,100),time:q.time})).slice(0,z);await d.autoScroll({times:Math.ceil(z/10),delayMs:2000});return(await d.evaluate(`(() => {
61
61
  const results = [];
62
62
  const items = document.querySelectorAll('[class*="_item_"]');
63
63
 
@@ -1,9 +1,9 @@
1
- import{cli as B,Strategy as C}from"@jackwener/opencli/registry";B({site:"jike",name:"repost",access:"write",description:"转发即刻帖子",domain:"web.okjike.com",strategy:C.UI,browser:!0,args:[{name:"id",type:"string",required:!0,positional:!0,help:"帖子 ID"},{name:"text",positional:!0,type:"string",required:!1,help:"转发附言(可选)"}],columns:["status","message"],func:async(b,q)=>{await b.goto(`https://web.okjike.com/originalPost/${q.id}`);for(let d=0;d<30;d++){if(await b.evaluate(`(() => {
1
+ import{cli as E,Strategy as F}from"@jackwener/opencli/registry";E({site:"jike",name:"repost",access:"write",description:"转发即刻帖子",domain:"web.okjike.com",strategy:F.UI,browser:!0,args:[{name:"id",type:"string",required:!0,positional:!0,help:"帖子 ID"},{name:"text",positional:!0,type:"string",required:!1,help:"转发附言(可选)"}],columns:["status","message"],func:async(b,v)=>{await b.goto(`https://web.okjike.com/originalPost/${v.id}`);for(let h=0;h<30;h++){if(await b.evaluate(`(() => {
2
2
  const actions = document.querySelector('[class*="_actions_"]');
3
3
  if (!actions) return false;
4
4
  const children = Array.from(actions.children).filter(c => c.offsetHeight > 0);
5
5
  return !!children[2];
6
- })()`))break;await b.wait(0.5)}const z=await b.evaluate(`(async () => {
6
+ })()`))break;await b.wait(0.5)}const A=await b.evaluate(`(async () => {
7
7
  try {
8
8
  const actions = document.querySelector('[class*="_actions_"]');
9
9
  if (!actions) return { ok: false, message: '未找到操作栏' };
@@ -15,11 +15,11 @@ import{cli as B,Strategy as C}from"@jackwener/opencli/registry";B({site:"jike",n
15
15
  } catch (e) {
16
16
  return { ok: false, message: e.toString() };
17
17
  }
18
- })()`);if(!z.ok)return[{status:"failed",message:z.message}];await b.wait(1);for(let d=0;d<30;d++){if(await b.evaluate(`(() => {
18
+ })()`);if(!A.ok)return[{status:"failed",message:A.message}];await b.wait(1);for(let h=0;h<30;h++){if(await b.evaluate(`(() => {
19
19
  return Array.from(document.querySelectorAll('button')).some(
20
20
  b => b.textContent?.trim() === '转发动态'
21
21
  );
22
- })()`))break;await b.wait(0.5)}const A=await b.evaluate(`(async () => {
22
+ })()`))break;await b.wait(0.5)}const B=await b.evaluate(`(async () => {
23
23
  try {
24
24
  const btn = Array.from(document.querySelectorAll('button')).find(
25
25
  b => b.textContent?.trim() === '转发动态'
@@ -30,13 +30,13 @@ import{cli as B,Strategy as C}from"@jackwener/opencli/registry";B({site:"jike",n
30
30
  } catch (e) {
31
31
  return { ok: false, message: e.toString() };
32
32
  }
33
- })()`);if(!A.ok)return[{status:"failed",message:A.message}];await b.wait(2);if(q.text){for(let h=0;h<30;h++){if(await b.evaluate(`(() => {
34
- return !!document.querySelector('[contenteditable="true"]');
35
- })()`))break;await b.wait(0.5)}const d=await b.evaluate(`(async () => {
33
+ })()`);if(!B.ok)return[{status:"failed",message:B.message}];await b.wait(2);if(v.text){for(let q=0;q<30;q++){if(await b.evaluate(`(() => {
34
+ return !!document.querySelector('[class*="Modal"] [contenteditable="true"], [class*="modal"] [contenteditable="true"]');
35
+ })()`))break;await b.wait(0.5)}const h=await b.evaluate(`(async () => {
36
36
  try {
37
- const textToInsert = ${JSON.stringify(q.text)};
38
- const editor = document.querySelector('[contenteditable="true"]');
39
- if (!editor) return { ok: false, message: '未找到附言输入框' };
37
+ const textToInsert = ${JSON.stringify(v.text)};
38
+ const editor = document.querySelector('[class*="Modal"] [contenteditable="true"], [class*="modal"] [contenteditable="true"]');
39
+ if (!editor) return { ok: false, message: '未找到转发弹窗内的附言输入框' };
40
40
  editor.focus();
41
41
  const dt = new DataTransfer();
42
42
  dt.setData('text/plain', textToInsert);
@@ -46,23 +46,22 @@ import{cli as B,Strategy as C}from"@jackwener/opencli/registry";B({site:"jike",n
46
46
  await new Promise(r => setTimeout(r, 500));
47
47
  return { ok: true };
48
48
  } catch(e) { return { ok: false, message: '附言写入失败: ' + e.toString() }; }
49
- })()`);if(!d.ok)return[{status:"failed",message:d.message}]}for(let d=0;d<30;d++){if(await b.evaluate(`(() => {
50
- return Array.from(document.querySelectorAll('button')).some(b => {
51
- const text = b.textContent?.trim() || '';
52
- return (text === '发送' || text === '发布') && !b.disabled;
53
- });
54
- })()`))break;await b.wait(0.5)}const v=await b.evaluate(`(async () => {
49
+ })()`);if(!h.ok)return[{status:"failed",message:h.message}]}const D=`(() => {
50
+ return Array.from(document.querySelectorAll('button')).find(b => {
51
+ const text = b.textContent?.trim() || '';
52
+ if (text !== '转发动态' && text !== '发送' && text !== '发布') return false;
53
+ if (b.disabled) return false;
54
+ if (b.closest('[class*="Popover"]')) return false;
55
+ return !!(b.closest('[class*="Modal"], [class*="modal"], [class*="_submitButton_"]'));
56
+ });
57
+ })`;for(let h=0;h<30;h++){if(await b.evaluate(`(() => !!${D}())()`))break;await b.wait(0.5)}const z=await b.evaluate(`(async () => {
55
58
  try {
56
59
  await new Promise(r => setTimeout(r, 500));
57
- const btn = Array.from(document.querySelectorAll('button')).find(b => {
58
- const text = b.textContent?.trim() || '';
59
- // 不匹配"转发动态",避免重复触发 Popover 菜单项
60
- return (text === '发送' || text === '发布') && !b.disabled;
61
- });
62
- if (!btn) return { ok: false, message: '未找到发送按钮' };
60
+ const btn = ${D}();
61
+ if (!btn) return { ok: false, message: '未找到转发弹窗内的确认按钮' };
63
62
  btn.click();
64
63
  return { ok: true, message: '转发成功' };
65
64
  } catch (e) {
66
65
  return { ok: false, message: e.toString() };
67
66
  }
68
- })()`);if(v.ok)await b.wait(3);return[{status:v.ok?"success":"failed",message:v.message}]}});
67
+ })()`);if(z.ok)await b.wait(3);return[{status:z.ok?"success":"failed",message:z.message}]}});
@@ -1,5 +1,5 @@
1
- import{cli as B,Strategy as C}from"@jackwener/opencli/registry";import{getPostDataJs as E}from"./utils.js";B({site:"jike",name:"search",access:"read",description:"搜索即刻帖子",domain:"web.okjike.com",strategy:C.COOKIE,browser:!0,args:[{name:"query",type:"string",required:!0,positional:!0,help:"即刻搜索关键词"},{name:"limit",type:"int",default:20}],columns:["id","author","content","likes","comments","time","url"],func:async(b,q)=>{const z=q.query,f=q.limit||20,A=encodeURIComponent(z);await b.goto(`https://web.okjike.com/search?q=${A}`);const v=async()=>{return await b.evaluate(`(() => {
2
- ${E}
1
+ import{cli as C,Strategy as F}from"@jackwener/opencli/registry";import{EmptyResultError as G}from"@jackwener/opencli/errors";import{getPostDataJs as H}from"./utils.js";C({site:"jike",name:"search",access:"read",description:"搜索即刻帖子",domain:"web.okjike.com",strategy:F.COOKIE,browser:!0,args:[{name:"query",type:"string",required:!0,positional:!0,help:"即刻搜索关键词"},{name:"limit",type:"int",default:20}],columns:["id","author","content","likes","comments","time","url"],func:async(f,v)=>{const z=v.query,h=v.limit||20,B=encodeURIComponent(z);await f.goto(`https://web.okjike.com/search?q=${B}`);const q=async()=>{return await f.evaluate(`(() => {
2
+ ${H}
3
3
 
4
4
  const results = [];
5
5
  const seen = new Set();
@@ -26,4 +26,4 @@ import{cli as B,Strategy as C}from"@jackwener/opencli/registry";import{getPostDa
26
26
  }
27
27
 
28
28
  return results;
29
- })()`)};let h=await v();if(h.length<f){await b.autoScroll({times:Math.ceil(f/10),delayMs:2000});h=await v()}return h.slice(0,f)}});
29
+ })()`)};let b=await q();for(let A=0;A<30&&b.length===0;A++){await f.wait(0.5);b=await q()}if(b.length===0)throw new G("jike search",`No results rendered for "${z}" within 15s. The keyword may have no matches or the page structure changed.`);if(b.length<h){await f.autoScroll({times:Math.ceil(h/10),delayMs:2000});b=await q()}return b.slice(0,h)}});
@@ -1,12 +1,12 @@
1
- import{cli as a,Strategy as zz}from"@jackwener/opencli/registry";import{CliError as L,CommandExecutionError as Gz}from"@jackwener/opencli/errors";import{readFile as Jz,stat as Kz}from"node:fs/promises";import{publishArticle as Qz}from"../_shared/article/publish.js";export const mediumProfile={home:"https://medium.com/new-story",outputFormat:"markdown",checkAuth:async(K)=>{try{const X=await(await fetch("https://medium.com/_/graphql",{method:"POST",credentials:"include",headers:{"content-type":"application/json",accept:"application/json"},body:JSON.stringify({operationName:"ViewerQuery",query:"query ViewerQuery { viewer { id name username imageId } }",variables:{}})})).json(),$=X&&X.data&&X.data.viewer;if($&&$.id)return{isAuthenticated:!0,userId:$.id,username:$.username||$.name||"",avatar:$.imageId?"https://miro.medium.com/v2/resize:fill:96:96/"+$.imageId:""};return{isAuthenticated:!1}}catch(V){return{isAuthenticated:!1,error:String(V&&V.message||V)}}},publish:async(K,V)=>{const X=(z)=>z.replace(/^\s*\]\)\}while\(1\);(<\/x>)?/,""),$=document.documentElement.outerHTML.match(/"xsrfToken":"([^"]+)"/),q=$?$[1]:null;if(!q)return{ok:!1,stage:"xsrf",message:"未能从编辑器页提取 xsrfToken(请确认已登录 medium.com 且停留在 /new-story)"};const C={accept:"application/json","x-requested-with":"XMLHttpRequest","x-xsrf-token":q},D=async(z,J,A)=>{const Z=Object.assign({},C);if(A!==void 0)Z["content-type"]="application/json";const W=await fetch("https://medium.com"+z,{method:J||"GET",credentials:"include",headers:Z,body:A!==void 0?JSON.stringify(A):void 0});let _=X(await W.text()),G=null;try{G=JSON.parse(_)}catch(F){}return{status:W.status,json:G,text:_}},M=(z)=>{const J=[];let A=0,Z="";const W=(_)=>{if(_)Z+=_};while(A<z.length){const G=z.slice(A).match(/^(\[([^\]]+)\]\(([^)]+)\)|\*\*([^*]+)\*\*|`([^`]+)`|\*([^*]+)\*|(https?:\/\/[^\s<]+[^\s<.,;:!?")\]]))/);if(!G){W(z[A]);A+=1;continue}const F=A+(G.index||0);if(F>A)W(z.slice(A,F));const H=G[1],O=Z.length;if(G[2]!==void 0&&G[3]!==void 0){W(G[2]);J.push({type:3,start:O,end:Z.length,href:G[3],title:"",rel:"nofollow",anchorType:0})}else if(G[4]!==void 0){W(G[4]);J.push({type:1,start:O,end:Z.length})}else if(G[5]!==void 0)W(G[5]);else if(G[6]!==void 0){W(G[6]);J.push({type:2,start:O,end:Z.length})}else if(G[7]!==void 0){W(G[7]);J.push({type:3,start:O,end:Z.length,href:G[7],title:"",rel:"nofollow",anchorType:0})}else W(H);A=F+H.length}return{text:Z,markups:J}},v=(z)=>{const J={paragraph:1,h1:12,h2:2,h3:3,h4:13,blockquote:6,code:8,"ul-li":9,"ol-li":10},A=(z||"").replace(/\r\n/g,`
1
+ import{cli as a,Strategy as zz}from"@jackwener/opencli/registry";import{CliError as L,CommandExecutionError as Gz}from"@jackwener/opencli/errors";import{readFile as Jz,stat as Kz}from"node:fs/promises";import{publishArticle as Qz}from"../_shared/article/publish.js";export const mediumProfile={home:"https://medium.com/new-story",outputFormat:"markdown",checkAuth:async(K)=>{try{const W=await(await fetch("https://medium.com/_/graphql",{method:"POST",credentials:"include",headers:{"content-type":"application/json",accept:"application/json"},body:JSON.stringify({operationName:"ViewerQuery",query:"query ViewerQuery { viewer { id name username imageId } }",variables:{}})})).json(),$=W&&W.data&&W.data.viewer;if($&&$.id)return{isAuthenticated:!0,userId:$.id,username:$.username||$.name||"",avatar:$.imageId?"https://miro.medium.com/v2/resize:fill:96:96/"+$.imageId:""};return{isAuthenticated:!1}}catch(Q){return{isAuthenticated:!1,error:String(Q&&Q.message||Q)}}},publish:async(K,Q)=>{const W=(z)=>z.replace(/^\s*\]\)\}while\(1\);(<\/x>)?/,""),$=document.documentElement.outerHTML.match(/"xsrfToken":"([^"]+)"/),q=$?$[1]:null;if(!q)return{ok:!1,stage:"xsrf",message:"未能从编辑器页提取 xsrfToken(请确认已登录 medium.com 且停留在 /new-story)"};const P={accept:"application/json","x-requested-with":"XMLHttpRequest","x-xsrf-token":q},D=async(z,J,A)=>{const X=Object.assign({},P);if(A!==void 0)X["content-type"]="application/json";const Z=await fetch("https://medium.com"+z,{method:J||"GET",credentials:"include",headers:X,body:A!==void 0?JSON.stringify(A):void 0});let F=W(await Z.text()),G=null;try{G=JSON.parse(F)}catch(U){}return{status:Z.status,json:G,text:F}},H=(z)=>{const J=[];let A=0,X="";const Z=(F)=>{if(F)X+=F};while(A<z.length){const G=z.slice(A).match(/^(\[([^\]]+)\]\(([^)]+)\)|\*\*([^*]+)\*\*|`([^`]+)`|\*([^*]+)\*|(https?:\/\/[^\s<]+[^\s<.,;:!?")\]]))/);if(!G){Z(z[A]);A+=1;continue}const U=A+(G.index||0);if(U>A)Z(z.slice(A,U));const S=G[1],O=X.length;if(G[2]!==void 0&&G[3]!==void 0){Z(G[2]);J.push({type:3,start:O,end:X.length,href:G[3],title:"",rel:"nofollow",anchorType:0})}else if(G[4]!==void 0){Z(G[4]);J.push({type:1,start:O,end:X.length})}else if(G[5]!==void 0)Z(G[5]);else if(G[6]!==void 0){Z(G[6]);J.push({type:2,start:O,end:X.length})}else if(G[7]!==void 0){Z(G[7]);J.push({type:3,start:O,end:X.length,href:G[7],title:"",rel:"nofollow",anchorType:0})}else Z(S);A=U+S.length}return{text:X,markups:J}},x=(z)=>{const J={paragraph:1,h1:12,h2:2,h3:3,h4:13,blockquote:6,code:8,"ul-li":9,"ol-li":10},A=(z||"").replace(/\r\n/g,`
2
2
  `).split(`
3
- `),Z=[];let W=[],_,G=[],F=!1;const H=(Q,S,N)=>{const T=(Q||"").trim();if(!T)return;const B=J[S]||J.paragraph,l=B===J.code?{text:T,markups:[]}:M(T);Z.push(Object.assign({type:B,text:l.text,markups:l.markups},N?{codeLang:N}:{}))},O=()=>{if(!W.length)return;const Q=W.join(" ").trim();W=[];if(!Q)return;const S=Q.match(/^(#{1,4})\s+(.*)$/);if(S){H(S[2].trim(),"h"+S[1].length);return}const N=Q.match(/^>\s?(.*)$/);if(N){H(N[1].trim(),"blockquote");return}const T=Q.match(/^\d+\.\s+(.*)$/);if(T){H(T[1].trim(),"ol-li");return}const B=Q.match(/^[-*]\s+(.*)$/);if(B){H(B[1].trim(),"ul-li");return}H(Q,"paragraph")},o=()=>{if(!G.length){_=void 0;return}Z.push(Object.assign({type:J.code,text:G.join(`
4
- `),markups:[]},_?{codeLang:_}:{}));G=[];_=void 0};for(const Q of A){const S=Q.trim().match(/^!\[([^\]]*)\]\(([^)\s]+)(?:\s+"[^"]*")?\)$/i),N=Q.trim().match(/^<img\b[^>]*?\bsrc=["']([^"']+)["'][^>]*>$/i);if(S){O();Z.push({image:!0,imageUrl:S[2],imageAlt:S[1]||""});continue}if(N){O();const B=Q.match(/\balt=["']([^"']+)["']/i);Z.push({image:!0,imageUrl:N[1],imageAlt:B&&B[1]||""});continue}const T=Q.match(/^```(\S+)?\s*$/);if(T){if(F){o();F=!1}else{O();F=!0;_=T[1]||void 0}continue}if(F){G.push(Q);continue}if(!Q.trim()){O();continue}if(/^(#{1,4})\s+/.test(Q)||/^>\s?/.test(Q)||/^\d+\.\s+/.test(Q)||/^[-*]\s+/.test(Q)){O();W.push(Q);O();continue}W.push(Q.trim())}O();if(F)o();return Z},P=async(z)=>{let J;const A=await fetch(z,{credentials:"omit"});if(!A.ok)throw Error("拉取图片失败 HTTP "+A.status);J=await A.blob();let Z="image.png";try{const O=new URL(z).pathname.split("/").filter(Boolean).pop()||"image.png";Z=/\.[a-z0-9]+$/i.test(O)?O:O+".png"}catch(H){}const W=new FormData;W.append("uploadedFile",J,Z);const _=await fetch("https://medium.com/_/upload?is2x=true",{method:"POST",credentials:"include",headers:{accept:"application/json","x-requested-with":"XMLHttpRequest","x-xsrf-token":q},body:W}),G=JSON.parse(X(await _.text())),F=G&&G.payload&&G.payload.value||{};if(!F.fileId||!F.imgWidth||!F.imgHeight)throw Error("上传响应缺 fileId/尺寸");return{fileId:F.fileId,w:F.imgWidth,h:F.imgHeight}},Y=[],j=[],x=await D("/new-story","POST",{}),h=x.json&&x.json.payload||{},U=h.id||h.value&&h.value.id;if(!U)return{ok:!1,stage:"create",status:x.status,message:"Medium 未返回草稿 postId:"+x.text.slice(0,160)};const n=await D("/_/api/posts/"+U+"/draft"),y=n.json&&n.json.payload||{},m=Array.isArray(y.normalizingDeltas)?y.normalizingDeltas:[],u=y.value&&typeof y.value.latestRev==="number"?y.value.latestRev:0;if(u<0&&m.length){const z=await D("/p/"+U+"/deltas","POST",{baseRev:u,deltas:m});if(z.status>=400)return{ok:!1,stage:"init",status:z.status,message:"初始化草稿失败"}}const c=await D("/_/api/posts/"+U),p=c.json&&c.json.payload&&c.json.payload.value||{},t=typeof p.latestRev==="number"?p.latestRev:0,R=K.params||{};let E=v(K.markdown||K.content||"");if(K.title&&E.length&&!E[0].image&&E[0].type===12&&(E[0].text||"").trim()===K.title.trim())E=E.slice(1);const b=[];if(K.title&&K.title.trim())b.push({type:3,text:K.title.trim(),markups:[]});if(R.subtitle&&String(R.subtitle).trim())b.push({type:4,text:String(R.subtitle).trim(),markups:[]});const r=b.concat(E),I=[];let w=0;for(const z of r){if(z.image){try{const J=await P(z.imageUrl);I.push({type:1,index:w,paragraph:{type:4,text:z.imageAlt||"",markups:[],layout:1,metadata:{id:J.fileId,originalWidth:J.w,originalHeight:J.h}}});Y.push(z.imageUrl);w+=1}catch(J){j.push({src:z.imageUrl,error:String(J&&J.message||J)})}continue}I.push(Object.assign({type:1,index:w,paragraph:Object.assign({type:z.type,text:z.text,markups:z.markups||[]},z.codeLang?{codeLang:z.codeLang}:{})}));w+=1}if(!I.length)return{ok:!1,stage:"content",message:"正文为空,无可写入的段落"};const f=await D("/p/"+U+"/deltas","POST",{baseRev:t,deltas:I});if(f.status>=400)return{ok:!1,stage:"write",status:f.status,message:"写入正文失败:"+f.text.slice(0,160),uploaded:Y,failed:j};const d=Array.isArray(R.tags)?R.tags.filter(Boolean).slice(0,5):[];if(d.length){const z=await D("/_/graphql","POST",{operationName:"SetPostTagsMutation",query:`mutation SetPostTagsMutation($targetPostId: ID!, $tagNames: [String!]!) {
3
+ `),X=[];let Z=[],F,G=[],U=!1;const S=(V,T,N)=>{const Y=(V||"").trim();if(!Y)return;const B=J[T]||J.paragraph,l=B===J.code?{text:Y,markups:[]}:H(Y);X.push(Object.assign({type:B,text:l.text,markups:l.markups},N?{codeLang:N}:{}))},O=()=>{if(!Z.length)return;const V=Z.join(" ").trim();Z=[];if(!V)return;const T=V.match(/^(#{1,4})\s+(.*)$/);if(T){S(T[2].trim(),"h"+T[1].length);return}const N=V.match(/^>\s?(.*)$/);if(N){S(N[1].trim(),"blockquote");return}const Y=V.match(/^\d+\.\s+(.*)$/);if(Y){S(Y[1].trim(),"ol-li");return}const B=V.match(/^[-*]\s+(.*)$/);if(B){S(B[1].trim(),"ul-li");return}S(V,"paragraph")},o=()=>{if(!G.length){F=void 0;return}X.push(Object.assign({type:J.code,text:G.join(`
4
+ `),markups:[]},F?{codeLang:F}:{}));G=[];F=void 0};for(const V of A){const T=V.trim().match(/^!\[([^\]]*)\]\(([^)\s]+)(?:\s+"[^"]*")?\)$/i),N=V.trim().match(/^<img\b[^>]*?\bsrc=["']([^"']+)["'][^>]*>$/i);if(T){O();X.push({image:!0,imageUrl:T[2],imageAlt:T[1]||""});continue}if(N){O();const B=V.match(/\balt=["']([^"']+)["']/i);X.push({image:!0,imageUrl:N[1],imageAlt:B&&B[1]||""});continue}const Y=V.match(/^```(\S+)?\s*$/);if(Y){if(U){o();U=!1}else{O();U=!0;F=Y[1]||void 0}continue}if(U){G.push(V);continue}if(!V.trim()){O();continue}if(/^(#{1,4})\s+/.test(V)||/^>\s?/.test(V)||/^\d+\.\s+/.test(V)||/^[-*]\s+/.test(V)){O();Z.push(V);O();continue}Z.push(V.trim())}O();if(U)o();return X},C=async(z)=>{let J,A="image.png";if(/^data:/i.test(z)){J=Q.dataUriToBlob(z);A="image."+(((J.type||"").split("/")[1]||"png").split("+")[0].split(";")[0]||"png")}else{const U=await fetch(z,{credentials:"omit"});if(!U.ok)throw Error("拉取图片失败 HTTP "+U.status);J=await U.blob();try{const O=new URL(z).pathname.split("/").filter(Boolean).pop()||"image.png";A=/\.[a-z0-9]+$/i.test(O)?O:O+".png"}catch(S){}}const X=new FormData;X.append("uploadedFile",J,A);const Z=await fetch("https://medium.com/_/upload?is2x=true",{method:"POST",credentials:"include",headers:{accept:"application/json","x-requested-with":"XMLHttpRequest","x-xsrf-token":q},body:X}),F=JSON.parse(W(await Z.text())),G=F&&F.payload&&F.payload.value||{};if(!G.fileId||!G.imgWidth||!G.imgHeight)throw Error("上传响应缺 fileId/尺寸");return{fileId:G.fileId,w:G.imgWidth,h:G.imgHeight}},j=[],M=[],y=await D("/new-story","POST",{}),w=y.json&&y.json.payload||{},_=w.id||w.value&&w.value.id;if(!_)return{ok:!1,stage:"create",status:y.status,message:"Medium 未返回草稿 postId:"+y.text.slice(0,160)};const n=await D("/_/api/posts/"+_+"/draft"),v=n.json&&n.json.payload||{},m=Array.isArray(v.normalizingDeltas)?v.normalizingDeltas:[],u=v.value&&typeof v.value.latestRev==="number"?v.value.latestRev:0;if(u<0&&m.length){const z=await D("/p/"+_+"/deltas","POST",{baseRev:u,deltas:m});if(z.status>=400)return{ok:!1,stage:"init",status:z.status,message:"初始化草稿失败"}}const b=await D("/_/api/posts/"+_),p=b.json&&b.json.payload&&b.json.payload.value||{},t=typeof p.latestRev==="number"?p.latestRev:0,R=K.params||{};let E=x(K.markdown||K.content||"");if(K.title&&E.length&&!E[0].image&&E[0].type===12&&(E[0].text||"").trim()===K.title.trim())E=E.slice(1);const c=[];if(K.title&&K.title.trim())c.push({type:3,text:K.title.trim(),markups:[]});if(R.subtitle&&String(R.subtitle).trim())c.push({type:4,text:String(R.subtitle).trim(),markups:[]});const r=c.concat(E),I=[];let h=0;for(const z of r){if(z.image){try{const J=await C(z.imageUrl);I.push({type:1,index:h,paragraph:{type:4,text:z.imageAlt||"",markups:[],layout:1,metadata:{id:J.fileId,originalWidth:J.w,originalHeight:J.h}}});j.push(z.imageUrl);h+=1}catch(J){M.push({src:z.imageUrl,error:String(J&&J.message||J)})}continue}I.push(Object.assign({type:1,index:h,paragraph:Object.assign({type:z.type,text:z.text,markups:z.markups||[]},z.codeLang?{codeLang:z.codeLang}:{})}));h+=1}if(!I.length)return{ok:!1,stage:"content",message:"正文为空,无可写入的段落"};const f=await D("/p/"+_+"/deltas","POST",{baseRev:t,deltas:I});if(f.status>=400)return{ok:!1,stage:"write",status:f.status,message:"写入正文失败:"+f.text.slice(0,160),uploaded:j,failed:M};const d=Array.isArray(R.tags)?R.tags.filter(Boolean).slice(0,5):[];if(d.length){const z=await D("/_/graphql","POST",{operationName:"SetPostTagsMutation",query:`mutation SetPostTagsMutation($targetPostId: ID!, $tagNames: [String!]!) {
5
5
  setPostTags(targetPostId: $targetPostId, tagNames: $tagNames) {
6
6
  id
7
7
  }
8
- }`,variables:{targetPostId:U,tagNames:d}});if(z.status>=400||z.json&&z.json.errors)j.push({src:"tags",error:"设置标签失败:"+(z.text||"").slice(0,120)})}if(R.canonicalUrl&&String(R.canonicalUrl).trim())await D("/_/graphql","POST",{operationName:"UpdateCanonicalUrl",query:`mutation UpdateCanonicalUrl($input: UpdateCanonicalUrlInput!) {
8
+ }`,variables:{targetPostId:_,tagNames:d}});if(z.status>=400||z.json&&z.json.errors)M.push({src:"tags",error:"设置标签失败:"+(z.text||"").slice(0,120)})}if(R.canonicalUrl&&String(R.canonicalUrl).trim())await D("/_/graphql","POST",{operationName:"UpdateCanonicalUrl",query:`mutation UpdateCanonicalUrl($input: UpdateCanonicalUrlInput!) {
9
9
  updateCanonicalUrl(input: $input) {
10
10
  __typename
11
11
  }
12
- }`,variables:{input:{postId:U,url:String(R.canonicalUrl).trim()}}});const s="https://medium.com/p/"+U+"/edit";if(K.draftOnly)return{ok:!0,id:U,url:s,draft:!0,uploaded:Y,failed:j};const k=await D("/p/"+U+"/publish","POST",{});if(k.status>=400)return{ok:!1,stage:"publish",status:k.status,message:"发布失败:"+k.text.slice(0,200),uploaded:Y,failed:j};const g=await D("/_/api/posts/"+U),i=g.json&&g.json.payload&&g.json.payload.value||{},e=i.mediumUrl||(i.uniqueSlug?"https://medium.com/p/"+U:"https://medium.com/p/"+U);return{ok:!0,id:U,url:e,draft:!1,uploaded:Y,failed:j}}},mediumAuthProfile={home:"https://medium.com/",checkAuth:mediumProfile.checkAuth};function Vz(K){if(!K.execute)throw new L("INVALID_INPUT","此命令需要 --execute 参数才能执行写操作")}async function Wz(K){const V=typeof K.text==="string"?K.text:void 0,X=typeof K.file==="string"?K.file:void 0;if(V&&X)throw new L("INVALID_INPUT","不能同时指定正文参数和 --file,请二选一");let $=V??"";if(X){let q;try{q=await Kz(X)}catch{throw new L("INVALID_INPUT","文件不存在: "+X)}if(!q.isFile())throw new L("INVALID_INPUT","必须是可读的文本文件: "+X);let C;try{C=await Jz(X)}catch{throw new L("INVALID_INPUT","无法读取文件: "+X)}try{$=new TextDecoder("utf-8",{fatal:!0}).decode(C)}catch{throw new L("INVALID_INPUT","文件必须是 UTF-8 编码: "+X)}}if(!$.trim())throw new L("INVALID_INPUT","正文不能为空");return $}function Xz(K,V,X,$,q={}){return[{status:"success",outcome:$,message:K,target_type:"article",target:V,...q}]}a({site:"medium",name:"article",access:"write",description:"发布 Medium 文章(走 medium.com 内部编辑器端点,非官方 API)。默认正式发布;加 --draft 仅存草稿。正文 Markdown,图片自动转存 Medium 图床。可选 --tags/--subtitle/--canonical-url。",domain:"medium.com",strategy:zz.COOKIE,browser:!0,args:[{name:"title",positional:!0,required:!0,help:"文章标题(作为 Medium 文章标题)"},{name:"text",positional:!0,help:"文章正文(Markdown)"},{name:"file",help:"正文文件路径(UTF-8,Markdown)"},{name:"tags",help:'Medium 话题标签,逗号分隔,最多 5 个(如 "AI,Programming,Open Source")'},{name:"subtitle",help:"副标题(可选,显示在标题下方)"},{name:"canonical-url",help:"原文规范链接(可选,内容首发别处时填,避免 SEO 重复)"},{name:"draft",type:"boolean",help:"仅保存草稿,不正式发布"},{name:"execute",type:"boolean",help:"确认执行写操作。不加此参数则拒绝写入。"}],columns:["status","outcome","message","target_type","target","created_target","created_url"],func:async(K,V)=>{if(!K)throw new Gz("Medium 文章发布需要浏览器会话");Vz(V);const X=String(V.title??"").trim();if(!X)throw new L("INVALID_INPUT","文章标题不能为空");const $=await Wz(V),q=Boolean(V.draft),D={tags:String(V.tags??"").split(",").map((j)=>j.trim()).filter(Boolean),subtitle:typeof V.subtitle==="string"?V.subtitle:"",canonicalUrl:typeof V["canonical-url"]==="string"?V["canonical-url"]:""},M=await Qz(K,{title:X,body:$,format:"markdown",draftOnly:q,profile:mediumProfile,publishParams:D}),v=M.images.uploaded.length|0,P=M.images.failed.length|0;let Y=M.draft?"Medium 草稿已保存(可在编辑器内手动发布)":"Medium 文章已正式发布";if(v||P)Y+=";图片:"+v+" 张已转存"+(P?","+P+" 张失败":"");return Xz(Y,"",M.url,M.draft?"draft":"publish",{created_target:"article:"+M.id,created_url:M.url})}});
12
+ }`,variables:{input:{postId:_,url:String(R.canonicalUrl).trim()}}});const s="https://medium.com/p/"+_+"/edit";if(K.draftOnly)return{ok:!0,id:_,url:s,draft:!0,uploaded:j,failed:M};const k=await D("/p/"+_+"/publish","POST",{});if(k.status>=400)return{ok:!1,stage:"publish",status:k.status,message:"发布失败:"+k.text.slice(0,200),uploaded:j,failed:M};const g=await D("/_/api/posts/"+_),i=g.json&&g.json.payload&&g.json.payload.value||{},e=i.mediumUrl||(i.uniqueSlug?"https://medium.com/p/"+_:"https://medium.com/p/"+_);return{ok:!0,id:_,url:e,draft:!1,uploaded:j,failed:M}}},mediumAuthProfile={home:"https://medium.com/",checkAuth:mediumProfile.checkAuth};function Vz(K){if(!K.execute)throw new L("INVALID_INPUT","此命令需要 --execute 参数才能执行写操作")}async function Wz(K){const Q=typeof K.text==="string"?K.text:void 0,W=typeof K.file==="string"?K.file:void 0;if(Q&&W)throw new L("INVALID_INPUT","不能同时指定正文参数和 --file,请二选一");let $=Q??"";if(W){let q;try{q=await Kz(W)}catch{throw new L("INVALID_INPUT","文件不存在: "+W)}if(!q.isFile())throw new L("INVALID_INPUT","必须是可读的文本文件: "+W);let P;try{P=await Jz(W)}catch{throw new L("INVALID_INPUT","无法读取文件: "+W)}try{$=new TextDecoder("utf-8",{fatal:!0}).decode(P)}catch{throw new L("INVALID_INPUT","文件必须是 UTF-8 编码: "+W)}}if(!$.trim())throw new L("INVALID_INPUT","正文不能为空");return $}function Xz(K,Q,W,$,q={}){return[{status:"success",outcome:$,message:K,target_type:"article",target:Q,...q}]}a({site:"medium",name:"article",access:"write",description:"发布 Medium 文章(走 medium.com 内部编辑器端点,非官方 API)。默认正式发布;加 --draft 仅存草稿。正文 Markdown,图片自动转存 Medium 图床。可选 --tags/--subtitle/--canonical-url。",domain:"medium.com",strategy:zz.COOKIE,browser:!0,args:[{name:"title",positional:!0,required:!0,help:"文章标题(作为 Medium 文章标题)"},{name:"text",positional:!0,help:"文章正文(Markdown)"},{name:"file",help:"正文文件路径(UTF-8,Markdown)"},{name:"tags",help:'Medium 话题标签,逗号分隔,最多 5 个(如 "AI,Programming,Open Source")'},{name:"subtitle",help:"副标题(可选,显示在标题下方)"},{name:"canonical-url",help:"原文规范链接(可选,内容首发别处时填,避免 SEO 重复)"},{name:"draft",type:"boolean",help:"仅保存草稿,不正式发布"},{name:"execute",type:"boolean",help:"确认执行写操作。不加此参数则拒绝写入。"}],columns:["status","outcome","message","target_type","target","created_target","created_url"],func:async(K,Q)=>{if(!K)throw new Gz("Medium 文章发布需要浏览器会话");Vz(Q);const W=String(Q.title??"").trim();if(!W)throw new L("INVALID_INPUT","文章标题不能为空");const $=await Wz(Q),q=Boolean(Q.draft),D={tags:String(Q.tags??"").split(",").map((y)=>y.trim()).filter(Boolean),subtitle:typeof Q.subtitle==="string"?Q.subtitle:"",canonicalUrl:typeof Q["canonical-url"]==="string"?Q["canonical-url"]:""},H=await Qz(K,{title:W,body:$,format:"markdown",draftOnly:q,profile:mediumProfile,publishParams:D}),x=H.images.uploaded.length|0,C=H.images.failed.length|0;let j=H.draft?"Medium 草稿已保存(可在编辑器内手动发布)":"Medium 文章已正式发布";if(x||C)j+=";图片:"+x+" 张已转存"+(C?","+C+" 张失败(失败明细见 images_failed)":"");const M=Xz(j,"",H.url,H.draft?"draft":"publish",{created_target:"article:"+H.id,created_url:H.url});if(C){M[0].status="partial";M[0].images_failed=JSON.stringify(H.images.failed)}return M}});
@@ -1 +1 @@
1
- import{CliError as X,CommandExecutionError as v}from"@jackwener/opencli/errors";import{cli as N,Strategy as b}from"@jackwener/opencli/registry";import{publishArticle as P,gotoWritePage as f}from"../_shared/article/publish.js";function R(){const K="0123456789abcdef";let G="";for(let z=0;z<32;z++)G+=K[Math.floor(Math.random()*K.length)];return G}export const sohuProfile={home:"https://mp.sohu.com/mpfe/v3/main/first/page?newsType=1",outputFormat:"html",preprocessConfig:{},checkAuth:async(K)=>{try{const z=await(await fetch("https://mp.sohu.com/mpbp/bp/account/list?_="+Date.now(),{method:"GET",credentials:"include"})).json();if(z.code!==2000000||!z.data?.data?.length)return{isAuthenticated:!1};const L=[];for(const Q of z.data.data)if(Q.accounts)L.push(...Q.accounts);if(!L.length)return{isAuthenticated:!1};const H=L[0],M=L.length>1?H.nickName+"(共"+L.length+"个子账号)":H.nickName;return{isAuthenticated:!0,userId:String(H.id),username:M,avatar:H.avatar||""}}catch(G){return{isAuthenticated:!1,error:String(G&&G.message||G)}}},image:{skip:["sohu.com"],uploadFn:async(K,G)=>{const L=await(await fetch("https://mp.sohu.com/mpbp/bp/account/list?_="+Date.now(),{method:"GET",credentials:"include"})).json();if(L.code!==2000000||!L.data?.data?.length)throw Error("获取搜狐账号信息失败,请确认已登录搜狐号");const H=[];for(const O of L.data.data)if(O.accounts)H.push(...O.accounts);if(!H.length)throw Error("搜狐号子账号列表为空");const M=String(H[0].id),Q=await fetch(K,{credentials:"omit"});if(!Q.ok)throw Error("图片下载失败("+Q.status+"):"+K);const Z=await Q.blob(),Y=new FormData;Y.append("file",Z,"image.jpg");Y.append("accountId",M);const $=await(await fetch("https://mp.sohu.com/commons/front/outerUpload/image/file?accountId="+M,{method:"POST",credentials:"include",body:Y})).json();if(!$.url)throw Error("搜狐图片上传失败:"+($.msg||JSON.stringify($).slice(0,100)));return{url:$.url}}},publish:async(K,G)=>{const z=K.params||{},L=await fetch("https://mp.sohu.com/mpbp/bp/account/list?_="+Date.now(),{method:"GET",credentials:"include"}),H=await L.json();if(H.code!==2000000||!H.data?.data?.length)return{ok:!1,stage:"auth",status:L.status,message:"获取搜狐账号信息失败,请确认已登录搜狐号"};const M=[];for(const B of H.data.data)if(B.accounts)M.push(...B.accounts);if(!M.length)return{ok:!1,stage:"auth",status:0,message:"搜狐号子账号列表为空"};const Q=Number(M[0].id),Z="0123456789abcdef";let Y="";for(let B=0;B<32;B++)Y+=Z[Math.floor(Math.random()*Z.length)];const F="100-"+Date.now()+"-"+Y,$=Number(z.channelId);if(!Number.isFinite($)||$<=0)return{ok:!1,stage:"params",status:0,message:"缺少合法 channelId(频道 id),请先用 `sohu channels` 取值"};const O=z.categoryId==null||z.categoryId===""?-1:Number(z.categoryId),q={title:K.title,brief:typeof z.brief==="string"?z.brief:"",content:K.content,channelId:$,categoryId:O,id:0,userColumnId:z.userColumnId==null||z.userColumnId==-1?0:Number(z.userColumnId),columnNewsIds:Array.isArray(z.columnNewsIds)?z.columnNewsIds:[],businessCode:0,declareOriginal:Boolean(z.declareOriginal),cover:typeof z.cover==="string"?z.cover:"",topicIds:Array.isArray(z.topicIds)?z.topicIds:[],isAd:0,userLabels:"[]",reprint:!1,customTags:typeof z.customTags==="string"?z.customTags:"",infoResource:0,sourceUrl:"",visibleToLoginedUsers:0,attrIds:Array.isArray(z.attrIds)?z.attrIds:[],accountId:Q};if(K.draftOnly){const B=Object.assign({},q,{auto:!0}),T=await fetch("https://mp.sohu.com/mpbp/bp/news/v4/news/draft/v2?accountId="+Q,{method:"POST",credentials:"include",headers:{"Content-Type":"application/json","X-Requested-With":"XMLHttpRequest","dv-id":Y,"sp-cm":F},body:JSON.stringify(B)}),j=await T.text();let _=null;try{_=JSON.parse(j)}catch(d){}if(!T.ok||!_||!_.success)return{ok:!1,stage:"draft",status:T.status,message:_&&_.msg||j.slice(0,300)};const x=String(_.data),C="https://mp.sohu.com/mpfe/v4/contentManagement/news/addarticle?spm=smmp.articlelist.0.0&contentStatus=2&id="+x;return{ok:!0,id:x,url:C,draft:!0}}const W=await fetch("https://mp.sohu.com/mpbp/bp/news/v4/news/publish/v2?accountId="+Q,{method:"POST",credentials:"include",headers:{"Content-Type":"application/json","X-Requested-With":"XMLHttpRequest","dv-id":Y,"sp-cm":F},body:JSON.stringify(q)}),S=await W.text();let V=null;try{V=JSON.parse(S)}catch(B){}if(!W.ok||!V||V.code!==2000000)return{ok:!1,stage:"publish",status:W.status,message:V&&(V.msg||V.message)||S.slice(0,300)};if(V.data!=null&&Object.prototype.toString.call(V.data)==="[object Object]")return{ok:!1,stage:"origin_check",status:W.status,message:"声明原创触发了原创校验(需在搜狐后台完成原创确认):"+JSON.stringify(V.data).slice(0,200)};const U=String(V.data),D="https://mp.sohu.com/mpfe/v4/contentManagement/news/addarticle?spm=smmp.articlelist.0.0&contentStatus=1&id="+U;return{ok:!0,id:U,url:D,draft:!1}}},sohuAuthProfile={home:sohuProfile.home,checkAuth:sohuProfile.checkAuth};function A(K){if(!K.execute)throw new X("INVALID_INPUT","此命令需要 --execute 才会真正写入,干跑请去掉该参数。")}async function y(K){const G=typeof K.text==="string"?K.text:void 0,z=typeof K.file==="string"?K.file:void 0;if(G&&z)throw new X("INVALID_INPUT","text 和 --file 只能二选一");let L=G??"";if(z){const{readFile:H,stat:M}=await import("node:fs/promises");let Q;try{Q=await M(z)}catch{throw new X("INVALID_INPUT","文件不存在:"+z)}if(!Q.isFile())throw new X("INVALID_INPUT","路径不是可读文本文件:"+z);let Z;try{Z=await H(z)}catch{throw new X("INVALID_INPUT","文件读取失败:"+z)}try{L=new TextDecoder("utf-8",{fatal:!0}).decode(Z)}catch{throw new X("INVALID_INPUT","文件不是合法 UTF-8 编码:"+z)}}if(!L.trim())throw new X("INVALID_INPUT","正文不能为空");return L}function E(K,G,z,L,H={}){return[{status:"success",outcome:L,message:K,target_type:G,target:z,...H}]}async function J(K,G,z){const L="(async () => {try {const r = await fetch("+JSON.stringify(G)+', { method: "GET", credentials: "include" });const t = await r.text();let j = null; try { j = JSON.parse(t); } catch (e) {}return { ok: r.ok, status: r.status, json: j, text: t.slice(0, 300) };} catch (e) { return { ok: false, status: 0, json: null, text: String((e && e.message) || e) }; }})()',H=await K.evaluate(L);if(!H||!H.ok||!H.json)throw new X("INVALID_INPUT","搜狐"+z+"接口请求失败(HTTP "+(H?H.status:"?")+"):"+(H?H.text:""));if(H.json.success===!1||H.json.code!=null&&H.json.code!==2000000&&H.json.code!==2000)throw new X("INVALID_INPUT","搜狐"+z+"接口返回错误:"+(H.json.msg||H.json.message||"未登录或无权限"));return H.json}async function k(K){const G=await J(K,"https://mp.sohu.com/mpbp/bp/account/list?_="+Date.now(),"账号列表"),z=G.data&&G.data.data||[],L=[];for(const H of z)if(H.accounts)L.push(...H.accounts);if(!L.length)throw new X("INVALID_INPUT","搜狐号子账号列表为空,请确认已登录搜狐号");return String(L[0].id)}async function I(K,G,z){const H=(await J(K,"https://mp.sohu.com/mpbp/bp/account/common/channels-data-api?status=1&accountId="+encodeURIComponent(G),"频道列举")).data||[],M=H.find((Q)=>String(Q.name)===String(z));if(!M)throw new X("INVALID_INPUT","未找到频道「"+z+"」,可选:"+(H.map((Q)=>Q.name).join(" / ")||"(空)")+"。用 `sohu channels` 查看完整列表。");return Number(M.id!=null?M.id:M.channelId)}async function m(K,G,z){const H=(await J(K,"https://mp.sohu.com/mpbp/bp/account/common/channels/"+G+"/categories","分类列举")).data||[],M=H.find((Q)=>String(Q.name)===String(z));if(!M)throw new X("INVALID_INPUT","频道(id="+G+")下未找到分类「"+z+"」,可选:"+(H.map((Q)=>Q.name).join(" / ")||"(该频道无分类)")+"。用 `sohu categories --channel <频道名>` 查看。");return Number(M.id)}async function h(K,G){const z=await J(K,"https://mp.sohu.com/mpbp/bp/account/column/v2/list","专栏列举"),L=z.data&&(z.data.data||z.data)||z.data||[],H=Array.isArray(L)?L:[],M=H.find((Q)=>String(Q.name)===String(G));if(!M)throw new X("INVALID_INPUT","未找到专栏「"+G+"」,可选:"+(H.map((Q)=>Q.name).join(" / ")||"(你还没有任何专栏)")+"。用 `sohu columns` 查看。");return Number(M.id)}async function w(K,G,z){const L=z.split(",").map((M)=>M.trim()).filter(Boolean);if(!L.length)return[];const H=[];for(const M of L){const Y=((await J(K,"https://mp.sohu.com/mpbp/bp/news/v4/label/topic/search?accountId="+G+"&keyword="+encodeURIComponent(M),"话题搜索")).data||[]).find((F)=>String(F.name)===String(M));if(!Y)throw new X("INVALID_INPUT","未找到话题「"+M+"」,请用 `sohu topics --keyword "+M+"` 搜索确认确切名称。");H.push(Y.id)}return H}N({site:"sohu",name:"article",access:"write",description:"发布文章到搜狐号(HTML 格式,外链图片自动转存到搜狐图床)。默认正式发布,加 --draft 仅存草稿。频道必填,合法值用 `sohu channels` 取。",domain:"mp.sohu.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 或 HTML)"},{name:"html",type:"boolean",help:"将正文视为原始 HTML 而非 Markdown"},{name:"channel",help:"频道名(精确匹配,必填),合法值用 `sohu channels` 列举"},{name:"category",help:"分类名(精确匹配,依赖所选频道),合法值用 `sohu categories --channel <频道名>` 列举;不传=不限"},{name:"column",help:"专栏名(精确匹配),合法值用 `sohu columns` 列举;不传=不归属专栏"},{name:"topics",help:"话题名,逗号分隔(每个精确匹配),用 `sohu topics` 搜索取确切名"},{name:"cover",help:"封面图 URL(须为搜狐图床地址);可空"},{name:"brief",help:"文章摘要;可空"},{name:"declare-original",type:"boolean",help:"声明原创(会触发搜狐原创校验,发布将被拦到原创确认流程)"},{name:"draft",type:"boolean",help:"仅保存草稿,不正式发布"},{name:"execute",type:"boolean",help:"实际执行写入操作;不加此参数则拒绝写入(干跑保护)"}],columns:["status","outcome","message","target_type","target","created_target","created_url"],func:async(K,G)=>{if(!K)throw new v("搜狐号文章发布需要浏览器会话");A(G);const z=String(G.title??"").trim();if(!z)throw new X("INVALID_INPUT","文章标题不能为空");const L=await y(G),H=Boolean(G.draft),M=typeof G.channel==="string"?G.channel.trim():"";if(!M)throw new X("INVALID_INPUT","缺少 --channel(频道名)。请先运行 `sohu channels` 取合法频道名再传入,不提供默认值。");await f(K,sohuProfile.home);const Q=await k(K),Z=await I(K,Q,M);let Y=-1;const F=typeof G.category==="string"?G.category.trim():"";if(F)Y=await m(K,Z,F);let $=0;const O=typeof G.column==="string"?G.column.trim():"";if(O)$=await h(K,O);let q=[];const W=typeof G.topics==="string"?G.topics.trim():"";if(W)q=await w(K,Q,W);const S={channelId:Z,categoryId:Y,userColumnId:$,topicIds:q,cover:typeof G.cover==="string"?G.cover.trim():"",brief:typeof G.brief==="string"?G.brief:"",declareOriginal:Boolean(G["declare-original"])},V=await P(K,{title:z,body:L,format:G.html?"html":"markdown",draftOnly:H,profile:sohuProfile,publishParams:S}),U=V.images.uploaded.length|0,D=V.images.failed.length|0;let B=V.draft?"已保存搜狐号草稿":"已正式发布到搜狐号";if(U||D)B+=`;图片:${U} 张已转存${D?`,${D} 张失败`:""}`;return E(B,"article","",V.draft?"draft":"created",{created_target:(V.draft?"draft:":"article:")+V.id,created_url:V.url})}});export const __test__={sohuProfile,requireExecute:A,buildResultRow:E,resolvePayload:y,generateDeviceId:R};
1
+ import{CliError as $,CommandExecutionError as v}from"@jackwener/opencli/errors";import{cli as N,Strategy as b}from"@jackwener/opencli/registry";import{publishArticle as f,gotoWritePage as P}from"../_shared/article/publish.js";import{SOHU_MPBP_HEADERS_JS as R}from"./mpbp.js";function k(){const Q="0123456789abcdef";let K="";for(let z=0;z<32;z++)K+=Q[Math.floor(Math.random()*Q.length)];return K}export const sohuProfile={home:"https://mp.sohu.com/mpfe/v3/main/first/page?newsType=1",outputFormat:"html",preprocessConfig:{},checkAuth:async(Q)=>{try{const z=await(await fetch("https://mp.sohu.com/mpbp/bp/account/list?_="+Date.now(),{method:"GET",credentials:"include"})).json();if(z.code!==2000000||!z.data?.data?.length)return{isAuthenticated:!1};const L=[];for(const V of z.data.data)if(V.accounts)L.push(...V.accounts);if(!L.length)return{isAuthenticated:!1};const G=L[0],X=L.length>1?G.nickName+"(共"+L.length+"个子账号)":G.nickName;return{isAuthenticated:!0,userId:String(G.id),username:X,avatar:G.avatar||""}}catch(K){return{isAuthenticated:!1,error:String(K&&K.message||K)}}},image:{skip:["sohu.com"],uploadFn:async(Q,K)=>{const L=await(await fetch("https://mp.sohu.com/mpbp/bp/account/list?_="+Date.now(),{method:"GET",credentials:"include"})).json();if(L.code!==2000000||!L.data?.data?.length)throw Error("获取搜狐账号信息失败,请确认已登录搜狐号");const G=[];for(const B of L.data.data)if(B.accounts)G.push(...B.accounts);if(!G.length)throw Error("搜狐号子账号列表为空");const X=String(G[0].id),V=await fetch(Q,{credentials:"omit"});if(!V.ok)throw Error("图片下载失败("+V.status+"):"+Q);const Y=await V.blob(),F=new FormData;F.append("file",Y,"image.jpg");F.append("accountId",X);const M=await(await fetch("https://mp.sohu.com/commons/front/outerUpload/image/file?accountId="+X,{method:"POST",credentials:"include",body:F})).json();if(!M.url)throw Error("搜狐图片上传失败:"+(M.msg||JSON.stringify(M).slice(0,100)));return{url:M.url}}},publish:async(Q,K)=>{const z=Q.params||{},L=await fetch("https://mp.sohu.com/mpbp/bp/account/list?_="+Date.now(),{method:"GET",credentials:"include"}),G=await L.json();if(G.code!==2000000||!G.data?.data?.length)return{ok:!1,stage:"auth",status:L.status,message:"获取搜狐账号信息失败,请确认已登录搜狐号"};const X=[];for(const W of G.data.data)if(W.accounts)X.push(...W.accounts);if(!X.length)return{ok:!1,stage:"auth",status:0,message:"搜狐号子账号列表为空"};const V=Number(X[0].id),Y="0123456789abcdef";let F="";for(let W=0;W<32;W++)F+=Y[Math.floor(Math.random()*Y.length)];const q="100-"+Date.now()+"-"+F,M=Number(z.channelId);if(!Number.isFinite(M)||M<=0)return{ok:!1,stage:"params",status:0,message:"缺少合法 channelId(频道 id),请先用 `sohu channels` 取值"};const B=z.categoryId==null||z.categoryId===""?-1:Number(z.categoryId),U={title:Q.title,brief:typeof z.brief==="string"?z.brief:"",content:Q.content,channelId:M,categoryId:B,id:0,userColumnId:z.userColumnId==null||z.userColumnId==-1?0:Number(z.userColumnId),columnNewsIds:Array.isArray(z.columnNewsIds)?z.columnNewsIds:[],businessCode:0,declareOriginal:Boolean(z.declareOriginal),cover:typeof z.cover==="string"?z.cover:"",topicIds:Array.isArray(z.topicIds)?z.topicIds:[],isAd:0,userLabels:"[]",reprint:!1,customTags:typeof z.customTags==="string"?z.customTags:"",infoResource:0,sourceUrl:"",visibleToLoginedUsers:0,attrIds:Array.isArray(z.attrIds)?z.attrIds:[],accountId:V};if(Q.draftOnly){const W=Object.assign({},U,{auto:!0}),S=await fetch("https://mp.sohu.com/mpbp/bp/news/v4/news/draft/v2?accountId="+V,{method:"POST",credentials:"include",headers:{"Content-Type":"application/json","X-Requested-With":"XMLHttpRequest","dv-id":F,"sp-cm":q},body:JSON.stringify(W)}),x=await S.text();let O=null;try{O=JSON.parse(x)}catch(i){}if(!S.ok||!O||!O.success)return{ok:!1,stage:"draft",status:S.status,message:O&&O.msg||x.slice(0,300)};const A=String(O.data),E="https://mp.sohu.com/mpfe/v4/contentManagement/news/addarticle?spm=smmp.articlelist.0.0&contentStatus=2&id="+A;return{ok:!0,id:A,url:E,draft:!0}}const H=await fetch("https://mp.sohu.com/mpbp/bp/news/v4/news/publish/v2?accountId="+V,{method:"POST",credentials:"include",headers:{"Content-Type":"application/json","X-Requested-With":"XMLHttpRequest","dv-id":F,"sp-cm":q},body:JSON.stringify(U)}),T=await H.text();let Z=null;try{Z=JSON.parse(T)}catch(W){}if(!H.ok||!Z||Z.code!==2000000)return{ok:!1,stage:"publish",status:H.status,message:Z&&(Z.msg||Z.message)||T.slice(0,300)};if(Z.data!=null&&Object.prototype.toString.call(Z.data)==="[object Object]")return{ok:!1,stage:"origin_check",status:H.status,message:"声明原创触发了原创校验(需在搜狐后台完成原创确认):"+JSON.stringify(Z.data).slice(0,200)};const D=String(Z.data),_="https://mp.sohu.com/mpfe/v4/contentManagement/news/addarticle?spm=smmp.articlelist.0.0&contentStatus=1&id="+D;return{ok:!0,id:D,url:_,draft:!1}}},sohuAuthProfile={home:sohuProfile.home,checkAuth:sohuProfile.checkAuth};function y(Q){if(!Q.execute)throw new $("INVALID_INPUT","此命令需要 --execute 才会真正写入,干跑请去掉该参数。")}async function j(Q){const K=typeof Q.text==="string"?Q.text:void 0,z=typeof Q.file==="string"?Q.file:void 0;if(K&&z)throw new $("INVALID_INPUT","text 和 --file 只能二选一");let L=K??"";if(z){const{readFile:G,stat:X}=await import("node:fs/promises");let V;try{V=await X(z)}catch{throw new $("INVALID_INPUT","文件不存在:"+z)}if(!V.isFile())throw new $("INVALID_INPUT","路径不是可读文本文件:"+z);let Y;try{Y=await G(z)}catch{throw new $("INVALID_INPUT","文件读取失败:"+z)}try{L=new TextDecoder("utf-8",{fatal:!0}).decode(Y)}catch{throw new $("INVALID_INPUT","文件不是合法 UTF-8 编码:"+z)}}if(!L.trim())throw new $("INVALID_INPUT","正文不能为空");return L}function C(Q,K,z,L,G={}){return[{status:"success",outcome:L,message:Q,target_type:K,target:z,...G}]}async function J(Q,K,z){const L="(async () => {try {"+R+"const r = await fetch("+JSON.stringify(K)+', { method: "GET", credentials: "include", headers: HDRS });const t = await r.text();let j = null; try { j = JSON.parse(t); } catch (e) {}return { ok: r.ok, status: r.status, json: j, text: t.slice(0, 300) };} catch (e) { return { ok: false, status: 0, json: null, text: String((e && e.message) || e) }; }})()',G=await Q.evaluate(L);if(!G||!G.ok||!G.json)throw new $("INVALID_INPUT","搜狐"+z+"接口请求失败(HTTP "+(G?G.status:"?")+"):"+(G?G.text:""));if(G.json.success===!1||G.json.code!=null&&G.json.code!==2000000&&G.json.code!==2000)throw new $("INVALID_INPUT","搜狐"+z+"接口返回错误:"+(G.json.msg||G.json.message||"未登录或无权限"));return G.json}async function m(Q){const K=await J(Q,"https://mp.sohu.com/mpbp/bp/account/list?_="+Date.now(),"账号列表"),z=K.data&&K.data.data||[],L=[];for(const G of z)if(G.accounts)L.push(...G.accounts);if(!L.length)throw new $("INVALID_INPUT","搜狐号子账号列表为空,请确认已登录搜狐号");return String(L[0].id)}async function I(Q,K,z){const L=await J(Q,"https://mp.sohu.com/mpbp/bp/account/common/channels-data-api?status=1&accountId="+encodeURIComponent(K),"频道列举"),G=Array.isArray(L)?L:L.data||[],X=G.find((V)=>String(V.name)===String(z));if(!X)throw new $("INVALID_INPUT","未找到频道「"+z+"」,可选:"+(G.map((V)=>V.name).join(" / ")||"(空)")+"。用 `sohu channels` 查看完整列表。");return Number(X.id!=null?X.id:X.channelId)}async function h(Q,K,z,L){const G=await J(Q,"https://mp.sohu.com/mpbp/bp/account/common/channels/"+z+"/categories?accountId="+encodeURIComponent(K),"分类列举"),X=Array.isArray(G)?G:G.data||[],V=X.find((Y)=>String(Y.name)===String(L));if(!V)throw new $("INVALID_INPUT","频道(id="+z+")下未找到分类「"+L+"」,可选:"+(X.map((Y)=>Y.name).join(" / ")||"(该频道无分类)")+"。用 `sohu categories --channel <频道名>` 查看。");return Number(V.id)}async function w(Q,K,z){const L=await J(Q,"https://mp.sohu.com/mpbp/bp/account/column/v2/list?accountId="+encodeURIComponent(K)+"&page=1&size=100","专栏列举"),G=Array.isArray(L)?L:L.data&&(L.data.data||L.data)||L.data||[],X=Array.isArray(G)?G:[],V=X.find((Y)=>String(Y.name)===String(z));if(!V)throw new $("INVALID_INPUT","未找到专栏「"+z+"」,可选:"+(X.map((Y)=>Y.name).join(" / ")||"(你还没有任何专栏)")+"。用 `sohu columns` 查看。");return Number(V.id)}async function d(Q,K,z){const L=z.split(",").map((X)=>X.trim()).filter(Boolean);if(!L.length)return[];const G=[];for(const X of L){const V=await J(Q,"https://mp.sohu.com/mpbp/bp/news/v4/label/topic/search?accountId="+K+"&keyword="+encodeURIComponent(X),"话题搜索"),F=(Array.isArray(V)?V:V.data||[]).find((q)=>String(q.name)===String(X));if(!F)throw new $("INVALID_INPUT","未找到话题「"+X+"」,请用 `sohu topics --keyword "+X+"` 搜索确认确切名称。");G.push(F.id)}return G}N({site:"sohu",name:"article",access:"write",description:"发布文章到搜狐号(HTML 格式,外链图片自动转存到搜狐图床)。默认正式发布,加 --draft 仅存草稿。频道必填,合法值用 `sohu channels` 取。",domain:"mp.sohu.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 或 HTML)"},{name:"html",type:"boolean",help:"将正文视为原始 HTML 而非 Markdown"},{name:"channel",help:"频道名(精确匹配,必填),合法值用 `sohu channels` 列举"},{name:"category",help:"分类名(精确匹配,依赖所选频道),合法值用 `sohu categories --channel <频道名>` 列举;不传=不限"},{name:"column",help:"专栏名(精确匹配),合法值用 `sohu columns` 列举;不传=不归属专栏"},{name:"topics",help:"话题名,逗号分隔(每个精确匹配),用 `sohu topics` 搜索取确切名"},{name:"cover",help:"封面图 URL(须为搜狐图床地址);可空"},{name:"brief",help:"文章摘要;可空"},{name:"declare-original",type:"boolean",help:"声明原创(会触发搜狐原创校验,发布将被拦到原创确认流程)"},{name:"draft",type:"boolean",help:"仅保存草稿,不正式发布"},{name:"execute",type:"boolean",help:"实际执行写入操作;不加此参数则拒绝写入(干跑保护)"}],columns:["status","outcome","message","target_type","target","created_target","created_url"],func:async(Q,K)=>{if(!Q)throw new v("搜狐号文章发布需要浏览器会话");y(K);const z=String(K.title??"").trim();if(!z)throw new $("INVALID_INPUT","文章标题不能为空");const L=await j(K),G=Boolean(K.draft),X=typeof K.channel==="string"?K.channel.trim():"";if(!X)throw new $("INVALID_INPUT","缺少 --channel(频道名)。请先运行 `sohu channels` 取合法频道名再传入,不提供默认值。");await P(Q,sohuProfile.home);const V=await m(Q),Y=await I(Q,V,X);let F=-1;const q=typeof K.category==="string"?K.category.trim():"";if(q)F=await h(Q,V,Y,q);let M=0;const B=typeof K.column==="string"?K.column.trim():"";if(B)M=await w(Q,V,B);let U=[];const H=typeof K.topics==="string"?K.topics.trim():"";if(H)U=await d(Q,V,H);const T={channelId:Y,categoryId:F,userColumnId:M,topicIds:U,cover:typeof K.cover==="string"?K.cover.trim():"",brief:typeof K.brief==="string"?K.brief:"",declareOriginal:Boolean(K["declare-original"])},Z=await f(Q,{title:z,body:L,format:K.html?"html":"markdown",draftOnly:G,profile:sohuProfile,publishParams:T}),D=Z.images.uploaded.length|0,_=Z.images.failed.length|0;let W=Z.draft?"已保存搜狐号草稿":"已正式发布到搜狐号";if(D||_)W+=`;图片:${D} 张已转存${_?`,${_} 张失败`:""}`;return C(W,"article","",Z.draft?"draft":"created",{created_target:(Z.draft?"draft:":"article:")+Z.id,created_url:Z.url})}});export const __test__={sohuProfile,requireExecute:y,buildResultRow:C,resolvePayload:j,generateDeviceId:k};
@@ -1 +1 @@
1
- import{cli as B,Strategy as D}from"@jackwener/opencli/registry";import{CliError as F,CommandExecutionError as G}from"@jackwener/opencli/errors";import{gotoWritePage as H}from"../_shared/article/publish.js";import{sohuProfile as J}from"./article.js";B({site:"sohu",name:"categories",access:"read",description:"列出某个搜狐号频道下的分类(id + 名称),供 `sohu article --category` 取合法分类名。须用 --channel 指定频道名。",domain:"mp.sohu.com",strategy:D.COOKIE,browser:!0,args:[{name:"channel",required:!0,help:"频道名(精确匹配),合法值用 `sohu channels` 列举"}],columns:["category_id","category_name"],func:async(q,A)=>{if(!q)throw new G("搜狐号分类列举需要浏览器会话");const v=String(A.channel??"").trim();if(!v)throw new F("INVALID_INPUT","请用 --channel 指定频道名");await H(q,J.home);const z=await q.evaluate("(async () => {const ar = await fetch('https://mp.sohu.com/mpbp/bp/account/list?_=' + Date.now(), { method: 'GET', credentials: 'include' });const aj = await ar.json();"+"if (!aj || aj.code !== 2000000 || !(aj.data && aj.data.data && aj.data.data.length)) throw new Error('获取搜狐账号失败,请确认已登录搜狐号');"+"let accountId = '';for (const g of aj.data.data) { if (g.accounts && g.accounts.length) { accountId = String(g.accounts[0].id); break; } }"+"if (!accountId) throw new Error('搜狐号子账号列表为空');"+"const cr = await fetch('https://mp.sohu.com/mpbp/bp/account/common/channels-data-api?status=1&accountId=' + accountId, { method: 'GET', credentials: 'include' });const cj = await cr.json();"+"if (!cj || cj.success === false) throw new Error('频道列举失败:' + ((cj && cj.msg) || '未登录或无权限'));"+"const channels = (cj && cj.data) || [];const ch = channels.find(c => String(c.name) === "+JSON.stringify(v)+");"+"if (!ch) throw new Error('未找到频道「"+v.replace(/"/g,"\\\"")+"」,可选:' + (channels.map(c => c.name).join(' / ') || '(空)'));"+"const channelId = ch.id != null ? ch.id : ch.channelId;const r = await fetch('https://mp.sohu.com/mpbp/bp/account/common/channels/' + channelId + '/categories', { method: 'GET', credentials: 'include' });const j = await r.json();"+"if (!j || j.success === false) throw new Error('分类列举失败:' + ((j && j.msg) || '未登录或无权限'));"+"return ((j && j.data) || []).map(c => ({ category_id: String(c.id), category_name: c.name || '' }));})()");return Array.isArray(z)?z:[]}});
1
+ import{cli as G,Strategy as K}from"@jackwener/opencli/registry";import{CliError as L,CommandExecutionError as Q}from"@jackwener/opencli/errors";import{gotoWritePage as T}from"../_shared/article/publish.js";import{sohuProfile as V}from"./article.js";import{SOHU_MPBP_HEADERS_JS as X}from"./mpbp.js";G({site:"sohu",name:"categories",access:"read",description:"列出某个搜狐号频道下的分类(id + 名称),供 `sohu article --category` 取合法分类名。须用 --channel 指定频道名。",domain:"mp.sohu.com",strategy:K.COOKIE,browser:!0,args:[{name:"channel",required:!0,help:"频道名(精确匹配),合法值用 `sohu channels` 列举"}],columns:["category_id","category_name"],func:async(q,F)=>{if(!q)throw new Q("搜狐号分类列举需要浏览器会话");const v=String(F.channel??"").trim();if(!v)throw new L("INVALID_INPUT","请用 --channel 指定频道名");await T(q,V.home);const z=await q.evaluate("(async () => {"+X+"const ar = await fetch('https://mp.sohu.com/mpbp/bp/account/list?_=' + Date.now(), { method: 'GET', credentials: 'include', headers: HDRS });const aj = await ar.json();"+"if (!aj || aj.code !== 2000000 || !(aj.data && aj.data.data && aj.data.data.length)) throw new Error('获取搜狐账号失败,请确认已登录搜狐号');"+"let accountId = '';for (const g of aj.data.data) { if (g.accounts && g.accounts.length) { accountId = String(g.accounts[0].id); break; } }"+"if (!accountId) throw new Error('搜狐号子账号列表为空');"+"const cr = await fetch('https://mp.sohu.com/mpbp/bp/account/common/channels-data-api?status=1&accountId=' + accountId, { method: 'GET', credentials: 'include', headers: HDRS });const cj = await cr.json();"+"if (!cj || cj.success === false) throw new Error('频道列举失败:' + ((cj && cj.msg) || '未登录或无权限'));"+"const channels = Array.isArray(cj) ? cj : ((cj && cj.data) || []);const ch = channels.find(c => String(c.name) === "+JSON.stringify(v)+");"+"if (!ch) throw new Error('未找到频道「"+v.replace(/"/g,"\\\"")+"」,可选:' + (channels.map(c => c.name).join(' / ') || '(空)'));"+"const channelId = ch.id != null ? ch.id : ch.channelId;const r = await fetch('https://mp.sohu.com/mpbp/bp/account/common/channels/' + channelId + '/categories?accountId=' + accountId, { method: 'GET', credentials: 'include', headers: HDRS });const j = await r.json();"+"if (!j || j.success === false) throw new Error('分类列举失败:' + ((j && j.msg) || '未登录或无权限'));"+"const list = Array.isArray(j) ? j : ((j && j.data) || []);return list.map(c => ({ category_id: String(c.id), category_name: c.name || '' }));})()");return Array.isArray(z)?z:[]}});
@@ -1 +1 @@
1
- import{cli as z,Strategy as A}from"@jackwener/opencli/registry";import{CommandExecutionError as B}from"@jackwener/opencli/errors";import{gotoWritePage as D}from"../_shared/article/publish.js";import{sohuProfile as F}from"./article.js";z({site:"sohu",name:"channels",access:"read",description:"列出当前搜狐号可发布的频道(id + 名称),供 `sohu article --channel` 取合法频道名。",domain:"mp.sohu.com",strategy:A.COOKIE,browser:!0,columns:["channel_id","channel_name"],func:async(q)=>{if(!q)throw new B("搜狐号频道列举需要浏览器会话");await D(q,F.home);const v=await q.evaluate("(async () => {const ar = await fetch('https://mp.sohu.com/mpbp/bp/account/list?_=' + Date.now(), { method: 'GET', credentials: 'include' });const aj = await ar.json();"+"if (!aj || aj.code !== 2000000 || !(aj.data && aj.data.data && aj.data.data.length)) throw new Error('获取搜狐账号失败,请确认已登录搜狐号');"+"let accountId = '';for (const g of aj.data.data) { if (g.accounts && g.accounts.length) { accountId = String(g.accounts[0].id); break; } }"+"if (!accountId) throw new Error('搜狐号子账号列表为空');"+"const r = await fetch('https://mp.sohu.com/mpbp/bp/account/common/channels-data-api?status=1&accountId=' + accountId, { method: 'GET', credentials: 'include' });const j = await r.json();"+"if (!j || j.success === false) throw new Error('频道列举失败:' + ((j && j.msg) || '未登录或无权限'));"+"return ((j && j.data) || []).map(c => ({ channel_id: String(c.id != null ? c.id : c.channelId), channel_name: c.name || '' }));})()");return Array.isArray(v)?v:[]}});
1
+ import{cli as z,Strategy as F}from"@jackwener/opencli/registry";import{CommandExecutionError as G}from"@jackwener/opencli/errors";import{gotoWritePage as I}from"../_shared/article/publish.js";import{sohuProfile as K}from"./article.js";import{SOHU_MPBP_HEADERS_JS as L}from"./mpbp.js";z({site:"sohu",name:"channels",access:"read",description:"列出当前搜狐号可发布的频道(id + 名称),供 `sohu article --channel` 取合法频道名。",domain:"mp.sohu.com",strategy:F.COOKIE,browser:!0,columns:["channel_id","channel_name"],func:async(q)=>{if(!q)throw new G("搜狐号频道列举需要浏览器会话");await I(q,K.home);const v=await q.evaluate("(async () => {"+L+"const ar = await fetch('https://mp.sohu.com/mpbp/bp/account/list?_=' + Date.now(), { method: 'GET', credentials: 'include', headers: HDRS });const aj = await ar.json();"+"if (!aj || aj.code !== 2000000 || !(aj.data && aj.data.data && aj.data.data.length)) throw new Error('获取搜狐账号失败,请确认已登录搜狐号');"+"let accountId = '';for (const g of aj.data.data) { if (g.accounts && g.accounts.length) { accountId = String(g.accounts[0].id); break; } }"+"if (!accountId) throw new Error('搜狐号子账号列表为空');"+"const r = await fetch('https://mp.sohu.com/mpbp/bp/account/common/channels-data-api?status=1&accountId=' + accountId, { method: 'GET', credentials: 'include', headers: HDRS });const j = await r.json();"+"if (!j || j.success === false) throw new Error('频道列举失败:' + ((j && j.msg) || '未登录或无权限'));"+"const list = Array.isArray(j) ? j : ((j && j.data) || []);return list.map(c => ({ channel_id: String(c.id != null ? c.id : c.channelId), channel_name: c.name || '' }));})()");return Array.isArray(v)?v:[]}});
@@ -1 +1 @@
1
- import{cli as z,Strategy as A}from"@jackwener/opencli/registry";import{CommandExecutionError as B}from"@jackwener/opencli/errors";import{gotoWritePage as D}from"../_shared/article/publish.js";import{sohuProfile as F}from"./article.js";z({site:"sohu",name:"columns",access:"read",description:"列出当前搜狐号的专栏(id + 名称),供 `sohu article --column` 取合法专栏名。",domain:"mp.sohu.com",strategy:A.COOKIE,browser:!0,columns:["column_id","column_name"],func:async(q)=>{if(!q)throw new B("搜狐号专栏列举需要浏览器会话");await D(q,F.home);const v=await q.evaluate("(async () => {const r = await fetch('https://mp.sohu.com/mpbp/bp/account/column/v2/list', { method: 'GET', credentials: 'include' });const j = await r.json();"+"if (!j || j.success === false) throw new Error('专栏列举失败:' + ((j && j.msg) || '未登录或无权限'));"+"const raw = (j && j.data) || [];const list = Array.isArray(raw) ? raw : (Array.isArray(raw.data) ? raw.data : []);return list.map(c => ({ column_id: String(c.id), column_name: c.name || '' }));})()");return Array.isArray(v)?v:[]}});
1
+ import{cli as z,Strategy as F}from"@jackwener/opencli/registry";import{CommandExecutionError as G}from"@jackwener/opencli/errors";import{gotoWritePage as I}from"../_shared/article/publish.js";import{sohuProfile as K}from"./article.js";import{SOHU_MPBP_HEADERS_JS as L}from"./mpbp.js";z({site:"sohu",name:"columns",access:"read",description:"列出当前搜狐号的专栏(id + 名称),供 `sohu article --column` 取合法专栏名。",domain:"mp.sohu.com",strategy:F.COOKIE,browser:!0,columns:["column_id","column_name"],func:async(q)=>{if(!q)throw new G("搜狐号专栏列举需要浏览器会话");await I(q,K.home);const v=await q.evaluate("(async () => {"+L+"const ar = await fetch('https://mp.sohu.com/mpbp/bp/account/list?_=' + Date.now(), { method: 'GET', credentials: 'include', headers: HDRS });const aj = await ar.json();"+"if (!aj || aj.code !== 2000000 || !(aj.data && aj.data.data && aj.data.data.length)) throw new Error('获取搜狐账号失败,请确认已登录搜狐号');"+"let accountId = '';for (const g of aj.data.data) { if (g.accounts && g.accounts.length) { accountId = String(g.accounts[0].id); break; } }"+"if (!accountId) throw new Error('搜狐号子账号列表为空');"+"const r = await fetch('https://mp.sohu.com/mpbp/bp/account/column/v2/list?accountId=' + accountId + '&page=1&size=100', { method: 'GET', credentials: 'include', headers: HDRS });const j = await r.json();"+"if (!j || j.success === false) throw new Error('专栏列举失败:' + ((j && j.msg) || '未登录或无权限'));"+"const raw = Array.isArray(j) ? j : ((j && j.data) || []);const list = Array.isArray(raw) ? raw : (Array.isArray(raw.data) ? raw.data : []);return list.map(c => ({ column_id: String(c.id), column_name: c.name || '' }));})()");return Array.isArray(v)?v:[]}});
@@ -0,0 +1 @@
1
+ export const SOHU_MPBP_HEADERS_JS="let __dvId = '', __spCm = '';try { __dvId = localStorage.getItem('preview-dv-id') || ''; __spCm = localStorage.getItem('preview-sp-cm') || ''; } catch (e) {}if (!__dvId || !__spCm) { const __chars = '0123456789abcdef'; let __gen = ''; for (let __i = 0; __i < 32; __i++) __gen += __chars[Math.floor(Math.random() * 16)]; if (!__dvId) __dvId = __gen; if (!__spCm) __spCm = '100-' + Date.now() + '-' + __gen;}const HDRS = { 'X-Requested-With': 'XMLHttpRequest', 'dv-id': __dvId, 'sp-cm': __spCm };";
@@ -1 +1 @@
1
- import{cli as B,Strategy as D}from"@jackwener/opencli/registry";import{CommandExecutionError as F}from"@jackwener/opencli/errors";import{gotoWritePage as G}from"../_shared/article/publish.js";import{sohuProfile as H}from"./article.js";B({site:"sohu",name:"topics",access:"read",description:"列举/搜索搜狐号话题(id + 名称 + 类型),供 `sohu article --topics` 取确切话题名。不传 --keyword 为推荐话题,传则按关键词搜索。",domain:"mp.sohu.com",strategy:D.COOKIE,browser:!0,args:[{name:"keyword",help:"搜索关键词;不传则返回推荐话题列表"}],columns:["topic_id","topic_name","topic_type"],func:async(q,v)=>{if(!q)throw new F("搜狐号话题列举需要浏览器会话");const A=typeof v.keyword==="string"?v.keyword.trim():"";await G(q,H.home);const z=await q.evaluate("(async () => {const ar = await fetch('https://mp.sohu.com/mpbp/bp/account/list?_=' + Date.now(), { method: 'GET', credentials: 'include' });const aj = await ar.json();"+"if (!aj || aj.code !== 2000000 || !(aj.data && aj.data.data && aj.data.data.length)) throw new Error('获取搜狐账号失败,请确认已登录搜狐号');"+"let accountId = '';for (const g of aj.data.data) { if (g.accounts && g.accounts.length) { accountId = String(g.accounts[0].id); break; } }"+"if (!accountId) throw new Error('搜狐号子账号列表为空');"+"const kw = "+JSON.stringify(A)+";const url = kw ? 'https://mp.sohu.com/mpbp/bp/news/v4/label/topic/search?accountId=' + accountId + '&keyword=' + encodeURIComponent(kw) : 'https://mp.sohu.com/mpbp/bp/news/v4/label/topic/list?accountId=' + accountId;const r = await fetch(url, { method: 'GET', credentials: 'include' });const j = await r.json();"+"if (!j || j.success === false) throw new Error('话题接口失败:' + ((j && j.msg) || '未登录或无权限'));"+"return ((j && j.data) || []).map(t => ({ topic_id: String(t.id), topic_name: t.name || '', topic_type: t.type != null ? String(t.type) : '' }));})()");return Array.isArray(z)?z:[]}});
1
+ import{cli as G,Strategy as I}from"@jackwener/opencli/registry";import{CommandExecutionError as K}from"@jackwener/opencli/errors";import{gotoWritePage as L}from"../_shared/article/publish.js";import{sohuProfile as N}from"./article.js";import{SOHU_MPBP_HEADERS_JS as Q}from"./mpbp.js";G({site:"sohu",name:"topics",access:"read",description:"列举/搜索搜狐号话题(id + 名称 + 类型),供 `sohu article --topics` 取确切话题名。不传 --keyword 为推荐话题,传则按关键词搜索。",domain:"mp.sohu.com",strategy:I.COOKIE,browser:!0,args:[{name:"keyword",help:"搜索关键词;不传则返回推荐话题列表"}],columns:["topic_id","topic_name","topic_type"],func:async(q,v)=>{if(!q)throw new K("搜狐号话题列举需要浏览器会话");const F=typeof v.keyword==="string"?v.keyword.trim():"";await L(q,N.home);const z=await q.evaluate("(async () => {"+Q+"const ar = await fetch('https://mp.sohu.com/mpbp/bp/account/list?_=' + Date.now(), { method: 'GET', credentials: 'include', headers: HDRS });const aj = await ar.json();"+"if (!aj || aj.code !== 2000000 || !(aj.data && aj.data.data && aj.data.data.length)) throw new Error('获取搜狐账号失败,请确认已登录搜狐号');"+"let accountId = '';for (const g of aj.data.data) { if (g.accounts && g.accounts.length) { accountId = String(g.accounts[0].id); break; } }"+"if (!accountId) throw new Error('搜狐号子账号列表为空');"+"const kw = "+JSON.stringify(F)+";const url = kw ? 'https://mp.sohu.com/mpbp/bp/news/v4/label/topic/search?accountId=' + accountId + '&keyword=' + encodeURIComponent(kw) : 'https://mp.sohu.com/mpbp/bp/news/v4/label/topic/list?accountId=' + accountId;const r = await fetch(url, { method: 'GET', credentials: 'include', headers: HDRS });const j = await r.json();"+"if (!j || j.success === false) throw new Error('话题接口失败:' + ((j && j.msg) || '未登录或无权限'));"+"const list = Array.isArray(j) ? j : ((j && j.data) || []);return list.map(t => ({ topic_id: String(t.id), topic_name: t.name || '', topic_type: t.type != null ? String(t.type) : '' }));})()");return Array.isArray(z)?z:[]}});
@@ -1 +1 @@
1
- import{CliError as V,CommandExecutionError as T}from"@jackwener/opencli/errors";import{cli as h,Strategy as P}from"@jackwener/opencli/registry";import{publishArticle as _}from"../_shared/article/publish.js";import{readFile as x,stat as y}from"node:fs/promises";function b(G){if(!G.execute)throw new V("INVALID_INPUT","此头条号写操作需要 --execute 参数才能真正发布")}async function C(G){const D=typeof G.text==="string"?G.text:void 0,z=typeof G.file==="string"?G.file:void 0;if(D&&z)throw new V("INVALID_INPUT","正文和 --file 只能选一个");let B=D??"";if(z){let M;try{M=await y(z)}catch{throw new V("INVALID_INPUT",`找不到文件: ${z}`)}if(!M.isFile())throw new V("INVALID_INPUT",`--file 必须是可读文本文件: ${z}`);let X;try{X=await x(z)}catch{throw new V("INVALID_INPUT",`文件无法读取: ${z}`)}try{B=new TextDecoder("utf-8",{fatal:!0}).decode(X)}catch{throw new V("INVALID_INPUT",`文件必须是 UTF-8 编码: ${z}`)}}if(!B.trim())throw new V("INVALID_INPUT","正文不能为空");return B}function E(G,D,z,B,M={}){return[{status:"success",outcome:B,message:G,target_type:D,target:z,...M}]}export const toutiaoProfile={home:"https://mp.toutiao.com/profile_v3/graphic/publish",outputFormat:"html",preprocessConfig:{},publish:async(G,D)=>{const z=G.params||{},B=[],M=[],X=["toutiao.com","pstatp.com","byteimg.com","bytecdn","ibytedtos","toutiaoimg","tos-cn"];async function W(J){const K=await fetch(J,{credentials:"omit"});if(!K.ok)throw Error("图片下载失败 HTTP "+K.status+":"+J.slice(0,80));const L=await K.blob(),S=((L.type||"").split("/")[1]||"jpg").split("+")[0].split(";")[0]||"jpg",H=new FormData;H.append("upfile",L,"image."+S);const U=await(await fetch("https://mp.toutiao.com/mp/agw/article_material/photo/upload_picture?type=ueditor&pgc_watermark=1&action=uploadimage&encode=utf-8",{method:"POST",credentials:"include",body:H})).json();if(U.code!==0||U.state!=="SUCCESS"||!U.url)throw Error(U.message||"图片上传失败");return{url:U.url,web_uri:U.origin_web_uri||U.web_uri,width:U.width,height:U.height}}const Y=document.createElement("div");Y.innerHTML=G.content;const Z=Array.prototype.slice.call(Y.querySelectorAll("img"));for(const J of Z){const K=J.getAttribute("src")||"";if(!K){J.remove();continue}if(X.some((L)=>K.indexOf(L)!==-1))continue;try{const L=await W(K);J.setAttribute("src",L.url);J.setAttribute("web_uri",L.web_uri);B.push({src:K.slice(0,120),url:L.url})}catch(L){M.push({src:K.slice(0,120),error:String(L&&L.message||L)})}}const $=Y.innerHTML,A=[];if(z.cover&&String(z.cover).trim()){let J;try{J=await W(String(z.cover))}catch(K){return{ok:!1,stage:"cover",message:"封面图转存失败:"+String(K&&K.message||K),uploaded:B,failed:M}}A.push({id:0,url:J.url,uri:J.web_uri,origin_uri:J.web_uri,ic_uri:"",thumb_width:J.width,thumb_height:J.height})}await fetch("https://mp.toutiao.com/profile_v3/graphic/publish",{credentials:"include"});const O=new URLSearchParams({title:G.title,article_ad_type:"2",article_type:"0",from_diagnosis:"0",origin_debut_check_pgc_normal:"0",tree_plan_article:"0",save:"0",pgc_id:"0",content:$,pgc_feed_covers:JSON.stringify(A)}),F=await fetch("https://mp.toutiao.com/mp/agw/article/publish?source=mp&type=article",{method:"POST",credentials:"include",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:O});let Q=null;try{Q=await F.json()}catch(J){}const R=Q&&(Q.code===0||Q.err_no===0),q=Q&&Q.data&&Q.data.pgc_id;if(!R||!q||String(q)==="0")return{ok:!1,stage:"publish",status:F.status,message:Q&&(Q.message||Q.reason)||"头条号发布失败",uploaded:B,failed:M};const N="https://mp.toutiao.com/profile_v3/graphic/publish?pgc_id="+String(q);return{ok:!0,id:String(q),url:N,draft:!1,uploaded:B,failed:M}},checkAuth:async(G)=>{try{const z=await(await fetch("https://mp.toutiao.com/mp/agw/media/get_media_info",{credentials:"include"})).json(),B=z&&z.data&&z.data.user;if(z&&z.code===0&&B&&B.id)return{isAuthenticated:!0,userId:String(B.id),username:B.screen_name||B.name||"",avatar:B.https_avatar_url||B.avatar_url||""};return{isAuthenticated:!1}}catch(D){return{isAuthenticated:!1,error:String(D&&D.message||D)}}}};h({site:"toutiao",name:"article",access:"write",description:"正式发布头条号图文文章(会立即公开,头条无可靠的存草稿接口,故不支持草稿)。正文默认 Markdown,图片自动转存到头条图床。标题需 2~30 字;封面可选(--cover,不传则头条自动从正文图选封面)。",domain:"mp.toutiao.com",strategy:P.COOKIE,browser:!0,args:[{name:"title",positional:!0,required:!0,help:"文章标题(2~30 字)"},{name:"text",positional:!0,help:"文章正文(默认 Markdown;传 --html 则视为原始 HTML)"},{name:"file",help:"正文文件路径(UTF-8,默认 Markdown)"},{name:"html",type:"boolean",help:"将正文视为原始 HTML 而非 Markdown"},{name:"cover",help:"封面图 URL 或本机路径(可选;会先转存到头条图床)"},{name:"execute",type:"boolean",help:"真正执行写操作。不加此参数时命令拒绝写入。"}],columns:["status","outcome","message","target_type","target","created_target","created_url"],func:async(G,D)=>{if(!G)throw new T("头条号文章发布需要浏览器会话");b(D);const z=String(D.title??"").trim();if(!z)throw new V("INVALID_INPUT","文章标题不能为空");if(z.length<2||z.length>30)throw new V("INVALID_INPUT",`头条号标题需 2~30 字,当前 ${z.length} 字`);const B=await C(D),X={cover:typeof D.cover==="string"?D.cover.trim():""},W=await _(G,{title:z,body:B,format:D.html?"html":"markdown",profile:toutiaoProfile,publishParams:X}),Y=W.images.uploaded.length|0,Z=W.images.failed.length|0;let $="已正式发布头条号文章";if(Y||Z)$+=`(图片:${Y} 张已转存${Z?`,${Z} 张失败`:""})`;return E($,"article","","created",{created_target:"article:"+W.id,created_url:W.url})}});
1
+ import{CliError as V,CommandExecutionError as T}from"@jackwener/opencli/errors";import{cli as h,Strategy as P}from"@jackwener/opencli/registry";import{publishArticle as _}from"../_shared/article/publish.js";import{readFile as x,stat as y}from"node:fs/promises";function b(J){if(!J.execute)throw new V("INVALID_INPUT","此头条号写操作需要 --execute 参数才能真正发布")}async function C(J){const G=typeof J.text==="string"?J.text:void 0,z=typeof J.file==="string"?J.file:void 0;if(G&&z)throw new V("INVALID_INPUT","正文和 --file 只能选一个");let B=G??"";if(z){let Q;try{Q=await y(z)}catch{throw new V("INVALID_INPUT",`找不到文件: ${z}`)}if(!Q.isFile())throw new V("INVALID_INPUT",`--file 必须是可读文本文件: ${z}`);let X;try{X=await x(z)}catch{throw new V("INVALID_INPUT",`文件无法读取: ${z}`)}try{B=new TextDecoder("utf-8",{fatal:!0}).decode(X)}catch{throw new V("INVALID_INPUT",`文件必须是 UTF-8 编码: ${z}`)}}if(!B.trim())throw new V("INVALID_INPUT","正文不能为空");return B}function E(J,G,z,B,Q={}){return[{status:"success",outcome:B,message:J,target_type:G,target:z,...Q}]}export const toutiaoProfile={home:"https://mp.toutiao.com/profile_v3/graphic/publish",outputFormat:"html",preprocessConfig:{},publish:async(J,G)=>{const z=J.params||{},B=[],Q=[],X=["toutiao.com","pstatp.com","byteimg.com","bytecdn","ibytedtos","toutiaoimg","tos-cn"];async function W(D){const L=await fetch(D,{credentials:"omit"});if(!L.ok)throw Error("图片下载失败 HTTP "+L.status+":"+D.slice(0,80));const M=await L.blob(),S=((M.type||"").split("/")[1]||"jpg").split("+")[0].split(";")[0]||"jpg",H=new FormData;H.append("upfile",M,"image."+S);const U=await(await fetch("https://mp.toutiao.com/mp/agw/article_material/photo/upload_picture?type=ueditor&pgc_watermark=1&action=uploadimage&encode=utf-8",{method:"POST",credentials:"include",body:H})).json();if(U.code!==0||U.state!=="SUCCESS"||!U.url)throw Error(U.message||"图片上传失败");return{url:U.url,web_uri:U.origin_web_uri||U.web_uri,width:U.width,height:U.height}}const Y=document.createElement("div");Y.innerHTML=J.content;const Z=Array.prototype.slice.call(Y.querySelectorAll("img"));for(const D of Z){const L=D.getAttribute("src")||"";if(!L){D.remove();continue}if(X.some((M)=>L.indexOf(M)!==-1))continue;try{const M=await W(L);D.setAttribute("src",M.url);D.setAttribute("web_uri",M.web_uri);B.push({src:L.slice(0,120),url:M.url})}catch(M){Q.push({src:L.slice(0,120),error:String(M&&M.message||M)})}}const $=Y.innerHTML,A=[];if(z.cover&&String(z.cover).trim()){let D;try{D=await W(String(z.cover))}catch(L){return{ok:!1,stage:"cover",message:"封面图转存失败:"+String(L&&L.message||L),uploaded:B,failed:Q}}A.push({id:0,url:D.url,uri:D.web_uri,origin_uri:D.web_uri,ic_uri:"",thumb_width:D.width,thumb_height:D.height})}await fetch("https://mp.toutiao.com/profile_v3/graphic/publish",{credentials:"include"});const O=new URLSearchParams({title:J.title,article_ad_type:"2",article_type:"0",from_diagnosis:"0",origin_debut_check_pgc_normal:"0",tree_plan_article:"0",save:"0",pgc_id:"0",content:$,pgc_feed_covers:JSON.stringify(A)}),F=await fetch("https://mp.toutiao.com/mp/agw/article/publish?source=mp&type=article",{method:"POST",credentials:"include",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:O});let K=null;try{K=await F.json()}catch(D){}const R=K&&(K.code===0||K.err_no===0),q=K&&K.data&&K.data.pgc_id;if(!R||!q||String(q)==="0"){let D=K&&(K.message||K.reason)||"头条号发布失败";if(K&&(K.code===7050||K.err_no===7050))D+="(code 7050:官方编辑器页发布同样被拒,通常为账号被平台限制发文或内容风控拦截,"+"请登录 mp.toutiao.com 查看站内通知并手动发一篇确认;读接口不受影响)";return{ok:!1,stage:"publish",status:F.status,message:D,detail:K,uploaded:B,failed:Q}}const N="https://mp.toutiao.com/profile_v3/graphic/publish?pgc_id="+String(q);return{ok:!0,id:String(q),url:N,draft:!1,uploaded:B,failed:Q}},checkAuth:async(J)=>{try{const z=await(await fetch("https://mp.toutiao.com/mp/agw/media/get_media_info",{credentials:"include"})).json(),B=z&&z.data&&z.data.user;if(z&&z.code===0&&B&&B.id)return{isAuthenticated:!0,userId:String(B.id),username:B.screen_name||B.name||"",avatar:B.https_avatar_url||B.avatar_url||""};return{isAuthenticated:!1}}catch(G){return{isAuthenticated:!1,error:String(G&&G.message||G)}}}};h({site:"toutiao",name:"article",access:"write",description:"正式发布头条号图文文章(会立即公开,头条无可靠的存草稿接口,故不支持草稿)。正文默认 Markdown,图片自动转存到头条图床。标题需 2~30 字;封面可选(--cover,不传则头条自动从正文图选封面)。",domain:"mp.toutiao.com",strategy:P.COOKIE,browser:!0,args:[{name:"title",positional:!0,required:!0,help:"文章标题(2~30 字)"},{name:"text",positional:!0,help:"文章正文(默认 Markdown;传 --html 则视为原始 HTML)"},{name:"file",help:"正文文件路径(UTF-8,默认 Markdown)"},{name:"html",type:"boolean",help:"将正文视为原始 HTML 而非 Markdown"},{name:"cover",help:"封面图 URL 或本机路径(可选;会先转存到头条图床)"},{name:"execute",type:"boolean",help:"真正执行写操作。不加此参数时命令拒绝写入。"}],columns:["status","outcome","message","target_type","target","created_target","created_url"],func:async(J,G)=>{if(!J)throw new T("头条号文章发布需要浏览器会话");b(G);const z=String(G.title??"").trim();if(!z)throw new V("INVALID_INPUT","文章标题不能为空");if(z.length<2||z.length>30)throw new V("INVALID_INPUT",`头条号标题需 2~30 字,当前 ${z.length} 字`);const B=await C(G),X={cover:typeof G.cover==="string"?G.cover.trim():""},W=await _(J,{title:z,body:B,format:G.html?"html":"markdown",profile:toutiaoProfile,publishParams:X}),Y=W.images.uploaded.length|0,Z=W.images.failed.length|0;let $="已正式发布头条号文章";if(Y||Z)$+=`(图片:${Y} 张已转存${Z?`,${Z} 张失败`:""})`;return E($,"article","","created",{created_target:"article:"+W.id,created_url:W.url})}});
@@ -1,8 +1,21 @@
1
- import{AuthRequiredError as t,CommandExecutionError as a}from"@jackwener/opencli/errors";import{registerSiteAuthCommands as u}from"../_shared/site-auth.js";async function i(e){return(await e.getCookies({url:"https://mp.toutiao.com"})).some((n)=>n.name==="sessionid"&&n.value)}async function s(e){if(!await i(e))throw new t("toutiao.com","Toutiao sessionid cookie missing");await e.goto("https://mp.toutiao.com/");await e.wait(2);const o=await e.evaluate(`
2
- (() => {
1
+ import{AuthRequiredError as D,CommandExecutionError as J}from"@jackwener/opencli/errors";import{registerSiteAuthCommands as K}from"../_shared/site-auth.js";async function F(z){return(await z.getCookies({url:"https://mp.toutiao.com"})).some((G)=>G.name==="sessionid"&&G.value)}async function H(z){if(!await F(z))throw new D("toutiao.com","Toutiao sessionid cookie missing");await z.goto("https://mp.toutiao.com/");await z.wait(2);const B=await z.evaluate(`
2
+ (async () => {
3
3
  if (/\\/auth\\/page\\/login/.test(location.href)) {
4
4
  return { kind: 'auth', detail: 'mp.toutiao.com redirected to /auth/page/login — anonymous' };
5
5
  }
6
+ try {
7
+ const resp = await fetch('https://mp.toutiao.com/mp/agw/media/get_media_info', { credentials: 'include' });
8
+ const json = await resp.json();
9
+ const u = json && json.data && json.data.user;
10
+ if (json && json.code === 0 && u && u.id) {
11
+ return { ok: true, user_id: String(u.id), nickname: u.screen_name || u.name || '' };
12
+ }
13
+ // 接口明确回未登录(非 0 code 且无用户)才判 auth;其余算接口异常,走下方 DOM 兜底
14
+ if (json && json.code !== 0 && !u) {
15
+ return { kind: 'maybe-auth', detail: 'get_media_info code=' + json.code + ' message=' + (json.message || '') };
16
+ }
17
+ } catch (e) {}
18
+ // 兜底:页面全局状态里找用户(get_media_info 网络异常时的次级信源)
6
19
  let userId = '', nickname = '';
7
20
  try {
8
21
  const seen = new Set();
@@ -30,8 +43,8 @@ import{AuthRequiredError as t,CommandExecutionError as a}from"@jackwener/opencli
30
43
  nickname = (el?.innerText || '').trim();
31
44
  }
32
45
  if (!userId) {
33
- return { kind: 'auth', detail: 'Toutiao dashboard rendered but no user_id surface stale session' };
46
+ return { kind: 'auth', detail: 'get_media_info 与页面状态均取不到用户身份会话可能已失效' };
34
47
  }
35
48
  return { ok: true, user_id: userId, nickname };
36
49
  })()
37
- `);if(o?.kind==="auth")throw new t("toutiao.com",o.detail);if(!o?.ok)throw new a(`Unexpected Toutiao probe: ${JSON.stringify(o)}`);return{user_id:o.user_id,nickname:o.nickname}}u({site:"toutiao",domain:"toutiao.com",loginUrl:"https://mp.toutiao.com/auth/page/login",columns:["user_id","nickname"],quickCheck:i,verify:s,poll:async(e)=>{if(!await i(e))throw new t("toutiao.com","Waiting for Toutiao sessionid cookie");return s(e)}});
50
+ `);if(B?.kind==="auth"||B?.kind==="maybe-auth")throw new D("toutiao.com",B.detail);if(!B?.ok)throw new J(`Unexpected Toutiao probe: ${JSON.stringify(B)}`);return{user_id:B.user_id,nickname:B.nickname}}K({site:"toutiao",domain:"toutiao.com",loginUrl:"https://mp.toutiao.com/auth/page/login",columns:["user_id","nickname"],quickCheck:F,verify:H,poll:async(z)=>{if(!await F(z))throw new D("toutiao.com","Waiting for Toutiao sessionid cookie");return H(z)}});
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "publishport-opencli",
3
- "version": "1.8.5-pp.22",
3
+ "version": "1.8.5-pp.24",
4
4
  "publishConfig": {
5
5
  "access": "public",
6
6
  "provenance": false