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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/cli-manifest.json CHANGED
@@ -33187,7 +33187,7 @@
33187
33187
  "name": "cover",
33188
33188
  "type": "str",
33189
33189
  "required": false,
33190
- "help": "封面图思否图床 url(选填)。需先把图片上传到思否图床得到 url。"
33190
+ "help": "封面图(选填):本机图片路径或图片 URL,自动转存到思否图床。"
33191
33191
  },
33192
33192
  {
33193
33193
  "name": "channels",
@@ -1,7 +1,7 @@
1
- import*as F from"node:fs";import*as W from"node:path";import{cli as M,Strategy as S}from"@jackwener/opencli/registry";import{ArgumentError as Z,CommandExecutionError as $}from"@jackwener/opencli/errors";import{assertLiteralContent as f}from"../_shared/content-guard.js";const C="https://creator.douyin.com/creator-micro/content/upload?default-tab=3",O=35,U=20,k=1000,A=2500,w={".jpg":"image/jpeg",".jpeg":"image/jpeg",".png":"image/png",".bmp":"image/bmp",".webp":"image/webp",".tif":"image/tiff",".tiff":"image/tiff"},X='input[type="file"][accept*="image"]',I=['input[placeholder="添加作品标题"]','input[placeholder*="标题"]'],_='input[placeholder="添加作品标题"]',x='.zone-container[contenteditable="true"]';function d(q){return q.map((z)=>{const J=W.resolve(z);if(!F.existsSync(J))throw new Z(`图片文件不存在: ${J}`);const Q=W.extname(J).toLowerCase();if(!w[Q])throw new Z(`不支持的图片格式 "${Q}",支持: jpg, png, bmp, webp, tif`);return J})}async function b(q,z=15000){const J=500,Q=Math.max(1,Math.ceil(z/J));for(let H=0;H<Q;H++){if(await q.evaluate(`(() => !!document.querySelector(${JSON.stringify(X)}))()`))return!0;if(H<Q-1)await q.wait({time:J/1000})}return!1}async function u(q,z){if(!await b(q))return{ok:!1,error:"上传页未渲染出图片输入框(等待 15s 超时)"};if(q.setFileInput)try{await q.setFileInput(z,X);return{ok:!0}}catch(H){const V=H instanceof Error?H.message:String(H);if(!(V.includes("Unknown action")||V.includes("not supported")||V.includes("Not allowed")))return{ok:!1,error:V}}const Q=z.map((H)=>({name:W.basename(H),mimeType:w[W.extname(H).toLowerCase()],base64:F.readFileSync(H).toString("base64")}));return q.evaluate(`
1
+ import*as R from"node:fs";import*as G from"node:path";import{cli as P,Strategy as h}from"@jackwener/opencli/registry";import{ArgumentError as $,CommandExecutionError as j}from"@jackwener/opencli/errors";import{assertLiteralContent as T}from"../_shared/content-guard.js";import{browserFetch as c}from"./_shared/browser-fetch.js";const m="https://creator.douyin.com/creator-micro/content/upload?default-tab=3",w=35,S=20,M=1000,E=2500,A={".jpg":"image/jpeg",".jpeg":"image/jpeg",".png":"image/png",".bmp":"image/bmp",".webp":"image/webp",".tif":"image/tiff",".tiff":"image/tiff"},D='input[type="file"][accept*="image"]',C=['input[placeholder="添加作品标题"]','input[placeholder*="标题"]'],l='input[placeholder="添加作品标题"]',_='.zone-container[contenteditable="true"]';function n(q){return q.map((J)=>{const K=G.resolve(J);if(!R.existsSync(K))throw new $(`图片文件不存在: ${K}`);const V=G.extname(K).toLowerCase();if(!A[V])throw new $(`不支持的图片格式 "${V}",支持: jpg, png, bmp, webp, tif`);return K})}async function o(q,J=15000){const K=500,V=Math.max(1,Math.ceil(J/K));for(let z=0;z<V;z++){if(await q.evaluate(`(() => !!document.querySelector(${JSON.stringify(D)}))()`))return!0;if(z<V-1)await q.wait({time:K/1000})}return!1}async function i(q,J){if(!await o(q))return{ok:!1,error:"上传页未渲染出图片输入框(等待 15s 超时)"};if(q.setFileInput)try{await q.setFileInput(J,D);return{ok:!0}}catch(z){const Z=z instanceof Error?z.message:String(z);if(!(Z.includes("Unknown action")||Z.includes("not supported")||Z.includes("Not allowed")))return{ok:!1,error:Z}}const V=J.map((z)=>({name:G.basename(z),mimeType:A[G.extname(z).toLowerCase()],base64:R.readFileSync(z).toString("base64")}));return q.evaluate(`
2
2
  (async () => {
3
- const images = ${JSON.stringify(Q)};
4
- const input = document.querySelector(${JSON.stringify(X)});
3
+ const images = ${JSON.stringify(V)};
4
+ const input = document.querySelector(${JSON.stringify(D)});
5
5
  if (!input) return { ok: false, error: '页面上找不到图片输入框' };
6
6
  const dt = new DataTransfer();
7
7
  for (const img of images) {
@@ -19,21 +19,21 @@ import*as F from"node:fs";import*as W from"node:path";import{cli as M,Strategy a
19
19
  input.dispatchEvent(new Event('input', { bubbles: true }));
20
20
  return { ok: true };
21
21
  })()
22
- `)}async function h(q,z=20000){const J=500,Q=Math.ceil(z/J);for(let H=0;H<Q;H++){if(await q.evaluate(`(() => {
22
+ `)}async function p(q,J=20000){const K=500,V=Math.ceil(J/K);for(let z=0;z<V;z++){if(await q.evaluate(`(() => {
23
23
  const onEditor = location.href.includes('/content/post/image');
24
- const titleReady = ${JSON.stringify(I)}.some((s) => !!document.querySelector(s));
24
+ const titleReady = ${JSON.stringify(C)}.some((s) => !!document.querySelector(s));
25
25
  return onEditor && titleReady;
26
- })()`))return!0;await q.wait({time:J/1000})}return!1}async function T(q,z){await q.typeText(_,z);await q.wait({time:0.3});const J=await q.evaluate(`
26
+ })()`))return!0;await q.wait({time:K/1000})}return!1}async function s(q,J){await q.typeText(l,J);await q.wait({time:0.3});const K=await q.evaluate(`
27
27
  ((selectors) => {
28
28
  const el = selectors.map((s) => document.querySelector(s)).find(Boolean);
29
29
  return el ? (el.value || '') : '';
30
- })(${JSON.stringify(I)})
31
- `);if(J.trim()!==z){await q.screenshot({path:"/tmp/douyin_image_title_debug.png"});throw new $(`填写标题失败,期望 "${z}",实际 "${J}"。截图: /tmp/douyin_image_title_debug.png`)}}async function P(q,z){await q.typeText(x,z);await q.wait({time:0.5});const J=await q.evaluate(`
30
+ })(${JSON.stringify(C)})
31
+ `);if(K.trim()!==J){await q.screenshot({path:"/tmp/douyin_image_title_debug.png"});throw new j(`填写标题失败,期望 "${J}",实际 "${K}"。截图: /tmp/douyin_image_title_debug.png`)}}async function g(q,J){await q.typeText(_,J);await q.wait({time:0.5});const K=await q.evaluate(`
32
32
  ((selector) => {
33
33
  const el = Array.from(document.querySelectorAll(selector)).find((n) => n && n.offsetParent !== null);
34
34
  return el ? (el.innerText || el.textContent || '').replace(/\\u200b/g, '').trim() : '';
35
- })(${JSON.stringify(x)})
36
- `),Q=z.split("#")[0].replace(/\s+/g,"").trim(),H=J.replace(/\s+/g,"");if(Q&&!H.includes(Q.slice(0,Math.min(Q.length,20)))){await q.screenshot({path:"/tmp/douyin_image_body_debug.png"});throw new $(`正文写入校验失败,实际读到 "${J.slice(0,80)}"。截图: /tmp/douyin_image_body_debug.png`)}}async function c(q,z){const J=z?["暂存离开"]:["发布"];return await q.evaluate(`
35
+ })(${JSON.stringify(_)})
36
+ `),V=J.split("#")[0].replace(/\s+/g,"").trim(),z=K.replace(/\s+/g,"");if(V&&!z.includes(V.slice(0,Math.min(V.length,20)))){await q.screenshot({path:"/tmp/douyin_image_body_debug.png"});throw new j(`正文写入校验失败,实际读到 "${K.slice(0,80)}"。截图: /tmp/douyin_image_body_debug.png`)}}async function r(q,J){const K=J?["暂存离开"]:["发布"];return await q.evaluate(`
37
37
  ((labels) => {
38
38
  const isVisible = (el) => {
39
39
  if (!el || el.offsetParent === null) return false;
@@ -48,15 +48,17 @@ import*as F from"node:fs";import*as W from"node:path";import{cli as M,Strategy a
48
48
  if (btn) { btn.click(); return { ok: true, text: label }; }
49
49
  }
50
50
  return { ok: false, seen: btns.filter(isVisible).map((b) => norm(b.innerText || b.textContent)).filter(Boolean).slice(0, 12) };
51
- })(${JSON.stringify(J)})
52
- `)}M({site:"douyin",name:"publish-image",access:"write",description:"发布图文(图集)到抖音(UI 自动化,最多 35 张图;话题以 #词 写进正文)",domain:"creator.douyin.com",strategy:S.COOKIE,browser:!0,navigateBefore:!1,defaultWindowMode:"foreground",args:[{name:"content",required:!1,positional:!0,help:"正文(字面文本,不展开 @文件 引用;长正文用 --file)"},{name:"file",required:!1,help:"从本机文件读取正文(UTF-8),与位置参数 <content> 二选一"},{name:"title",required:!0,help:"作品标题(≤20字)"},{name:"images",required:!0,help:"图片路径,逗号分隔,最多35张 (jpg/png/bmp/webp/tif)"},{name:"topics",required:!1,help:"话题标签,逗号分隔,不含 # 号"},{name:"draft",type:"bool",default:!1,help:"暂存为草稿,不直接发布"}],columns:["status","detail"],func:async(q,z)=>{if(!q)throw Error("Browser page required");const J=String(z.title??"").trim(),Q=z.file?String(z.file).trim():"";let H=String(z.content??"").trim();if(Q){if(H)throw new Z("正文位置参数和 --file 只能二选一");const K=W.resolve(Q);if(!F.existsSync(K))throw new Z(`--file 指向的文件不存在: ${K}`);H=F.readFileSync(K,"utf-8").trim();if(!H)throw new Z(`--file 指向的文件内容为空: ${K}`)}const V=String(z.images??"").split(",").map((K)=>K.trim()).filter(Boolean),v=z.topics?String(z.topics).split(",").map((K)=>K.trim().replace(/^#/,"")).filter(Boolean):[],Y=Boolean(z.draft);if(!J)throw new Z("--title 必填");if(J.length>U)throw new Z(`标题 ${J.length} 字,需 ≤ ${U} 字`);if(!H)throw new Z("缺少正文:给位置参数 <content>,或用 --file <本机文件路径>");f(H,{fileFlag:"--file"});if(H.length>k)throw new Z(`正文 ${H.length} 字,需 ≤ ${k} 字`);if(V.length===0)throw new Z("至少提供 1 张图片(--images)");if(V.length>O)throw new Z(`图片过多: ${V.length}(最多 ${O} 张)`);const y=d(V),L=v.length?`${H} ${v.map((K)=>`#${K}`).join(" ")} `:H;await q.goto(C);let j="";for(let K=0;K<30;K++){await q.wait({time:0.5});j=await q.evaluate("() => location.href");if(j.includes("creator.douyin.com/creator-micro/content"))break}if(!j.includes("creator.douyin.com")){await q.screenshot({path:"/tmp/douyin_image_redirect_debug.png"});throw new $(`被重定向离开创作者中心(当前 ${j})——登录态可能已过期。`+"用 opencli douyin login 重新登录。截图: /tmp/douyin_image_redirect_debug.png")}const R=await u(q,y);if(!R.ok){await q.screenshot({path:"/tmp/douyin_image_upload_debug.png"});throw new $(`图片上传失败: ${R.error??"unknown"}。截图: /tmp/douyin_image_upload_debug.png`)}if(!await h(q)){await q.screenshot({path:"/tmp/douyin_image_form_debug.png"});throw new $("上传后编辑页未就绪(标题输入框未出现),页面结构可能变化。截图: /tmp/douyin_image_form_debug.png")}await q.wait({time:A/1000});await T(q,J);await q.wait({time:0.4});await P(q,L);await q.wait({time:0.6});const D=await c(q,Y);if(!D?.ok){await q.screenshot({path:"/tmp/douyin_image_submit_debug.png"});throw new $(`找不到「${Y?"暂存离开":"发布"}」按钮。可见按钮: ${(D?.seen||[]).join(" | ")||"无"}。截图: /tmp/douyin_image_submit_debug.png`)}let B=j,G=!1;for(let K=0;K<24;K++){await q.wait({time:0.5});B=await q.evaluate("() => location.href");if(B.includes("/content/manage")||Y&&B.includes("/content/draft")){G=!0;break}}const N=G?"":await q.evaluate(`
53
- ((markers) => {
54
- for (const el of document.querySelectorAll('*')) {
55
- if (el.tagName === 'STYLE' || el.tagName === 'SCRIPT') continue;
56
- if (el.children.length !== 0) continue;
57
- const text = (el.innerText || '').trim();
58
- if (text && text.length <= 40 && markers.some((m) => text.includes(m))) return text;
59
- }
60
- return '';
61
- })(${JSON.stringify(Y?["暂存成功","保存成功","草稿"]:["发布成功","提交成功","发布"])})
62
- `);if(!G&&!N){await q.screenshot({path:"/tmp/douyin_image_verify_debug.png"});throw new $(`${Y?"暂存":"发布"}结果无法确认:未跳转作品管理页、也未见成功提示。当前 URL: ${B}。截图: /tmp/douyin_image_verify_debug.png`)}return[{status:`✅ ${Y?"暂存成功":"发布成功"}`,detail:[`"${J}"`,`${y.length}张图片`,v.length?`话题: ${v.map((K)=>`#${K}`).join(" ")}`:"",G?B:N].filter(Boolean).join(" · ")}]}});
51
+ })(${JSON.stringify(K)})
52
+ `)}P({site:"douyin",name:"publish-image",access:"write",description:"发布图文(图集)到抖音(UI 自动化,最多 35 张图;话题以 #词 写进正文)",domain:"creator.douyin.com",strategy:h.COOKIE,browser:!0,navigateBefore:!1,defaultWindowMode:"foreground",args:[{name:"content",required:!1,positional:!0,help:"正文(字面文本,不展开 @文件 引用;长正文用 --file)"},{name:"file",required:!1,help:"从本机文件读取正文(UTF-8),与位置参数 <content> 二选一"},{name:"title",required:!0,help:"作品标题(≤20字)"},{name:"images",required:!0,help:"图片路径,逗号分隔,最多35张 (jpg/png/bmp/webp/tif)"},{name:"topics",required:!1,help:"话题标签,逗号分隔,不含 # 号"},{name:"draft",type:"bool",default:!1,help:"暂存为草稿,不直接发布"}],columns:["status","detail"],func:async(q,J)=>{if(!q)throw Error("Browser page required");const K=String(J.title??"").trim(),V=J.file?String(J.file).trim():"";let z=String(J.content??"").trim();if(V){if(z)throw new $("正文位置参数和 --file 只能二选一");const Q=G.resolve(V);if(!R.existsSync(Q))throw new $(`--file 指向的文件不存在: ${Q}`);z=R.readFileSync(Q,"utf-8").trim();if(!z)throw new $(`--file 指向的文件内容为空: ${Q}`)}const Z=String(J.images??"").split(",").map((Q)=>Q.trim()).filter(Boolean),F=J.topics?String(J.topics).split(",").map((Q)=>Q.trim().replace(/^#/,"")).filter(Boolean):[],Y=Boolean(J.draft);if(!K)throw new $("--title 必填");if(K.length>S)throw new $(`标题 ${K.length} 字,需 ≤ ${S} 字`);if(!z)throw new $("缺少正文:给位置参数 <content>,或用 --file <本机文件路径>");T(z,{fileFlag:"--file"});if(z.length>M)throw new $(`正文 ${z.length} 字,需 ≤ ${M} 字`);if(Z.length===0)throw new $("至少提供 1 张图片(--images)");if(Z.length>w)throw new $(`图片过多: ${Z.length}(最多 ${w} 张)`);const O=n(Z),f=F.length?`${z} ${F.map((Q)=>`#${Q}`).join(" ")} `:z;await q.goto(m);let W="";for(let Q=0;Q<30;Q++){await q.wait({time:0.5});W=await q.evaluate("() => location.href");if(W.includes("creator.douyin.com/creator-micro/content"))break}if(!W.includes("creator.douyin.com")){await q.screenshot({path:"/tmp/douyin_image_redirect_debug.png"});throw new j(`被重定向离开创作者中心(当前 ${W})——登录态可能已过期。`+"用 opencli douyin login 重新登录。截图: /tmp/douyin_image_redirect_debug.png")}const k=await i(q,O);if(!k.ok){await q.screenshot({path:"/tmp/douyin_image_upload_debug.png"});throw new j(`图片上传失败: ${k.error??"unknown"}。截图: /tmp/douyin_image_upload_debug.png`)}if(!await p(q)){await q.screenshot({path:"/tmp/douyin_image_form_debug.png"});throw new j("上传后编辑页未就绪(标题输入框未出现),页面结构可能变化。截图: /tmp/douyin_image_form_debug.png")}await q.wait({time:E/1000});await s(q,K);await q.wait({time:0.4});await g(q,f);await q.wait({time:0.6});const I=await r(q,Y);if(!I?.ok){await q.screenshot({path:"/tmp/douyin_image_submit_debug.png"});throw new j(`找不到「${Y?"暂存离开":"发布"}」按钮。可见按钮: ${(I?.seen||[]).join(" | ")||"无"}。截图: /tmp/douyin_image_submit_debug.png`)}const d=Y?["暂存成功","保存成功","存入草稿"]:["发布成功","提交成功"];let B=W,X=!1,v="";for(let Q=0;Q<24;Q++){await q.wait({time:0.5});const H=await q.evaluate(`
53
+ ((markers) => {
54
+ const url = location.href;
55
+ let toast = '';
56
+ for (const el of document.querySelectorAll('*')) {
57
+ if (el.tagName === 'STYLE' || el.tagName === 'SCRIPT') continue;
58
+ if (el.children.length !== 0) continue;
59
+ const text = (el.innerText || '').trim();
60
+ if (text && text.length <= 40 && markers.some((m) => text.includes(m))) { toast = text; break; }
61
+ }
62
+ return { url, toast };
63
+ })(${JSON.stringify(d)})
64
+ `);B=H?.url??B;if(H?.toast){v=H.toast;break}if(B.includes("/content/manage")||Y&&B.includes("/content/draft")){X=!0;break}}let y=null;if(!X&&!v&&B.includes("creator.douyin.com"))try{const H=await c(q,"GET",Y?"https://creator.douyin.com/web/api/media/aweme/draft/?aid=1128":"https://creator.douyin.com/janus/douyin/creator/pc/work_list?page_size=20&page_num=1&status=0"),u=H.aweme_list??H.data?.work_list??[],U=z.replace(/\s+/g,"").slice(0,20),b=Date.now()/1000;y=u.find((N)=>{const x=String(N.desc??N.title??"").replace(/\s+/g,""),L=Number(N.create_time??0);return L>0&&b-L<900&&(x.includes(K.replace(/\s+/g,""))||U&&x.includes(U))})??null}catch{}if(!X&&!v&&!y){await q.screenshot({path:"/tmp/douyin_image_verify_debug.png"});throw new j(`${Y?"暂存":"发布"}结果无法确认:未跳转作品管理页、未见成功提示,`+`后台${Y?"草稿箱":"作品列表"}回查也没找到本次内容——大概率没发出去,可以安全重试。`+`当前 URL: ${B}。截图: /tmp/douyin_image_verify_debug.png`)}return[{status:`✅ ${Y?"暂存成功":"发布成功"}`,detail:[`"${K}"`,`${O.length}张图片`,F.length?`话题: ${F.map((Q)=>`#${Q}`).join(" ")}`:"",y?.aweme_id?`aweme_id: ${y.aweme_id}(后台回查坐实)`:"",X?B:v].filter(Boolean).join(" · ")}]}});
@@ -1,10 +1,10 @@
1
- import{cli as U,Strategy as W}from"@jackwener/opencli/registry";import{CliError as Y}from"@jackwener/opencli/errors";const $=10,I=30;function J(f,D){return`
1
+ import{cli as Y,Strategy as $}from"@jackwener/opencli/registry";import{CliError as I}from"@jackwener/opencli/errors";const J=10,X=30;function Z(h,H){return`
2
2
  async () => {
3
3
  const API = 'https://apiv1.oschina.net/oschinapi';
4
- const LIMIT = ${JSON.stringify(f)};
5
- const USER_ARG = ${JSON.stringify(D)};
6
- const PAGE_SIZE = ${$};
7
- const MAX_PAGES = ${I};
4
+ const LIMIT = ${JSON.stringify(h)};
5
+ const USER_ARG = ${JSON.stringify(H)};
6
+ const PAGE_SIZE = ${J};
7
+ const MAX_PAGES = ${X};
8
8
  const getJson = async (url) => {
9
9
  const resp = await fetch(url, { credentials: 'include' });
10
10
  const text = await resp.text();
@@ -51,4 +51,4 @@ import{cli as U,Strategy as W}from"@jackwener/opencli/registry";import{CliError
51
51
  }
52
52
  return out;
53
53
  }
54
- `}function X(f){if(f&&typeof f==="object"&&!Array.isArray(f)&&"session"in f&&"data"in f)return f.data;return f}function Q(f){return typeof f==="number"&&Number.isFinite(f)?f:""}function T(f,D,L,N){const h=f.id!=null?String(f.id):"",H=N&&h?N.get(h):null,z=(F)=>f[F]!=null?f[F]:H?H[F]:void 0,O=Number(z("type")),B={};for(const[F,q]of Object.entries({reply_count:Q(z("replyCount")),state:z("state"),as_recomm:f.asRecomm,detritus:z("detritus"),scheduled_time:f.scheduledTime,scheduled_state:f.scheduledState,summary:typeof z("detail")==="string"?String(z("detail")).slice(0,80):void 0}))if(q!==void 0&&q!==null&&q!=="")B[F]=q;return{id:h,type:"博客",title:f.title!=null?String(f.title):"",url:h&&D?`https://my.oschina.net/u/${D}/blog/${h}`:"",published_at:z("createTime")||"",views:Q(z("viewCount")),likes:Q(z("voteCount")),comments:Q(z("commentCount")),collects:"",shares:"",origin:O===2?"转载":O===1?"原创":"",is_private:Number(z("isPrivate"))===1?"":"否",recommended:Number(z("recomm"))===1?"推荐":"",top:Number(z("asTop"))===1?"置顶":"",status:L,extra:Object.keys(B).length?JSON.stringify(B):""}}U({site:"oschina",name:"stats",access:"read",description:"开源中国博客数据(每篇阅读/点赞/评论等全量运营指标)",domain:"my.oschina.net",strategy:W.COOKIE,browser:!0,args:[{name:"limit",type:"number",default:20,help:"返回条数"},{name:"user",help:"查看指定用户 ID 的公开博客数据(默认当前登录账号,含私密与定时博客)"}],columns:["id","type","title","url","published_at","views","likes","comments","collects","shares","origin","is_private","recommended","top","status","extra"],func:async(f,D)=>{const L=Number(D.limit)||20,N=D.user!=null&&String(D.user).trim()!==""?String(D.user).trim():"";await f.goto("https://my.oschina.net/");await f.wait(1);const h=X(await f.evaluate(J(L,N)));if(!h||typeof h!=="object")throw Error("开源中国页面取数失败:未拿到任何响应,请确认 Chrome 已打开且能访问 my.oschina.net。");if(h.notLoggedIn)throw Error("未登录开源中国:user/myDetails 未返回用户信息。请先在 Chrome 中登录 my.oschina.net(可用 opencli oschina login)。");const H=String(h.targetUser||h.me||""),z=h.pub&&h.pub.rows||[];if(h.pub&&h.pub.error)throw Error("获取开源中国博客公开列表失败:"+h.pub.error);const O=new Map(z.filter((q)=>q&&q.id!=null).map((q)=>[String(q.id),q])),B=[],F=new Set;if(h.isSelf){if(h.mine&&h.mine.error)throw Error("获取开源中国「我的创作」博客列表失败:"+h.mine.error);for(const q of h.mine&&h.mine.rows||[]){const K=T(q,H,"已发布",O);if(K.id)F.add(K.id);B.push(K)}for(const q of z){const K=q&&q.id!=null?String(q.id):"";if(!K||F.has(K))continue;F.add(K);B.push(T(q,H,"已发布",null))}for(const q of h.scheduled&&h.scheduled.rows||[])B.push(T(q,H,"已定时未发布",null))}else for(const q of z)B.push(T(q,H,"已发布",null));if(B.length===0)throw new Y("EMPTY_RESULT",h.isSelf?"当前开源中国账号没有任何博客(已发布/定时均为空)。":`用户 ${H} 没有任何公开博客。`,h.isSelf?"发布过内容后再来查看数据(opencli oschina article)。":"请确认用户 ID 是否正确。");return B.slice(0,L)}});
54
+ `}function f(h){if(h&&typeof h==="object"&&!Array.isArray(h)&&"session"in h&&"data"in h)return h.data;return h}function U(h){return typeof h==="number"&&Number.isFinite(h)?h:""}function W(h,H,Q,T,q){const F=h.id!=null?String(h.id):"",N=T&&F?T.get(F):null,B=(z)=>h[z]!=null?h[z]:N?N[z]:void 0,K=Number(B("type")),O={};for(const[z,D]of Object.entries({reply_count:U(B("replyCount")),state:B("state"),as_recomm:h.asRecomm,detritus:B("detritus"),scheduled_time:h.scheduledTime,scheduled_state:h.scheduledState,summary:typeof B("detail")==="string"?String(B("detail")).slice(0,80):void 0}))if(D!==void 0&&D!==null&&D!=="")O[z]=D;return{id:F,type:"博客",title:h.title!=null?String(h.title):"",url:F&&H?`https://my.oschina.net/u/${H}/blog/${F}`:"",published_at:B("createTime")||"",views:U(B("viewCount")),likes:U(B("voteCount")),comments:U(B("commentCount")),collects:"",shares:"",origin:K===2?"转载":K===1?"原创":"",is_private:Q!=="已发布"?"":q||N?"否":"是",recommended:Number(B("recomm"))===1?"推荐":"",top:Number(B("asTop"))===1?"置顶":"",status:Q,extra:Object.keys(O).length?JSON.stringify(O):""}}Y({site:"oschina",name:"stats",access:"read",description:"开源中国博客数据(每篇阅读/点赞/评论等全量运营指标)",domain:"my.oschina.net",strategy:$.COOKIE,browser:!0,args:[{name:"limit",type:"number",default:20,help:"返回条数"},{name:"user",help:"查看指定用户 ID 的公开博客数据(默认当前登录账号,含私密与定时博客)"}],columns:["id","type","title","url","published_at","views","likes","comments","collects","shares","origin","is_private","recommended","top","status","extra"],func:async(h,H)=>{const Q=Number(H.limit)||20,T=H.user!=null&&String(H.user).trim()!==""?String(H.user).trim():"";await h.goto("https://my.oschina.net/");await h.wait(1);const q=f(await h.evaluate(Z(Q,T)));if(!q||typeof q!=="object")throw Error("开源中国页面取数失败:未拿到任何响应,请确认 Chrome 已打开且能访问 my.oschina.net。");if(q.notLoggedIn)throw Error("未登录开源中国:user/myDetails 未返回用户信息。请先在 Chrome 中登录 my.oschina.net(可用 opencli oschina login)。");const F=String(q.targetUser||q.me||""),N=q.pub&&q.pub.rows||[];if(q.pub&&q.pub.error)throw Error("获取开源中国博客公开列表失败:"+q.pub.error);const B=new Map(N.filter((z)=>z&&z.id!=null).map((z)=>[String(z.id),z])),K=[],O=new Set;if(q.isSelf){if(q.mine&&q.mine.error)throw Error("获取开源中国「我的创作」博客列表失败:"+q.mine.error);for(const z of q.mine&&q.mine.rows||[]){const D=W(z,F,"已发布",B);if(D.id)O.add(D.id);K.push(D)}for(const z of N){const D=z&&z.id!=null?String(z.id):"";if(!D||O.has(D))continue;O.add(D);K.push(W(z,F,"已发布",null,!0))}for(const z of q.scheduled&&q.scheduled.rows||[])K.push(W(z,F,"已定时未发布",null,!1))}else for(const z of N)K.push(W(z,F,"已发布",null,!0));if(K.length===0)throw new I("EMPTY_RESULT",q.isSelf?"当前开源中国账号没有任何博客(已发布/定时均为空)。":`用户 ${F} 没有任何公开博客。`,q.isSelf?"发布过内容后再来查看数据(opencli oschina article)。":"请确认用户 ID 是否正确。");return K.slice(0,Q)}});
@@ -1,7 +1,7 @@
1
- import*as h from"node:fs";import{cli as V,Strategy as v}from"@jackwener/opencli/registry";import{ArgumentError as W,CommandExecutionError as X}from"@jackwener/opencli/errors";import{qiitaGql as Z,qiitaDeleteByForm as H}from"./gql.js";import{assertLiteralContent as R}from"../_shared/content-guard.js";export function resolveBody(z){if(z["body-file"]){const J=String(z["body-file"]);if(!h.statSync(J,{throwIfNoEntry:!1})?.isFile())throw new W(`--body-file not found: ${J}`);return h.readFileSync(J,"utf8")}if(z.body!==void 0&&z.body!==null){R(String(z.body),{fileFlag:"--body-file"});return String(z.body)}return}export function resolveTagNames(z){return String(z??"").split(",").map((J)=>J.trim()).filter(Boolean)}const G=`mutation($input: SaveCreatingArticleInput!) {
1
+ import*as V from"node:fs";import{cli as v,Strategy as F}from"@jackwener/opencli/registry";import{ArgumentError as X,CommandExecutionError as Y}from"@jackwener/opencli/errors";import{qiitaGql as Z,qiitaDeleteByForm as R}from"./gql.js";import{assertLiteralContent as h}from"../_shared/content-guard.js";export function resolveBody(z){if(z["body-file"]){const J=String(z["body-file"]);if(!V.statSync(J,{throwIfNoEntry:!1})?.isFile())throw new X(`--body-file not found: ${J}`);return V.readFileSync(J,"utf8")}if(z.body!==void 0&&z.body!==null){h(String(z.body),{fileFlag:"--body-file"});return String(z.body)}return}export function assertNoLocalImages(z){const J=[],M=(K)=>{const O=String(K??"").trim();if(!O)return;if(/^(https?:)?\/\//i.test(O))return;J.push(O.length>80?O.slice(0,80)+"…":O)};for(const K of z.matchAll(/!\[[^\]]*\]\(\s*<?([^)\s>]+)/g))M(K[1]);for(const K of z.matchAll(/<img\b[^>]*\bsrc\s*=\s*["']([^"']+)["']/gi))M(K[1]);if(J.length)throw new X(`正文里有 ${J.length} 张非公网 URL 的图片(${J.slice(0,3).join("、")})。`+"Qiita 不转存本机图片,发出去会是裂图——请改用公网可直接访问的 http(s) 图片 URL,或移除这些图。")}export function resolveTagNames(z){return String(z??"").split(",").map((J)=>J.trim()).filter(Boolean)}const G=`mutation($input: SaveCreatingArticleInput!) {
2
2
  saveCreatingArticle(input: $input) { draftItem { uuid } }
3
- }`,I=`mutation($input: PublishPublicArticleInput!) {
3
+ }`,P=`mutation($input: PublishPublicArticleInput!) {
4
4
  publishPublicArticle(input: $input) { article { uuid linkUrl encryptedId isSecret } }
5
- }`,P=`mutation($input: PublishSecretArticleInput!) {
5
+ }`,T=`mutation($input: PublishSecretArticleInput!) {
6
6
  publishSecretArticle(input: $input) { article { uuid linkUrl encryptedId isSecret } }
7
- }`;V({site:"qiita",name:"publish",access:"write",description:"Publish a Qiita article (markdown). Tags required. Public by default; --draft or --secret to change.",domain:"qiita.com",strategy:v.COOKIE,browser:!0,args:[{name:"title",type:"string",required:!1,help:"Article title (required)"},{name:"body",type:"string",required:!1,help:"Article body in Markdown (or use --body-file)"},{name:"body-file",type:"string",required:!1,help:"Path to a Markdown file for the body"},{name:"tags",type:"string",required:!1,help:"Comma-separated tags (REQUIRED, at least 1)"},{name:"draft",type:"boolean",required:!1,default:!1,help:"Save as draft only (do not publish)"},{name:"secret",type:"boolean",required:!1,default:!1,help:"Publish as limited-public (secret) instead of public"},{name:"tweet-share",type:"boolean",required:!1,default:!1,help:"Also share to X/Twitter on publish (public only)"}],columns:["status","uuid","url"],func:async(z,J)=>{if(!z)throw new X("Browser session required for qiita publish");const K=String(J.title??"").trim(),M=resolveBody(J),Y=resolveTagNames(J.tags);if(!K)throw new W("--title is required");if(M===void 0)throw new W("--body or --body-file is required");if(Y.length===0)throw new W("--tags is required (Qiita articles need at least one tag)");await z.goto("https://qiita.com/drafts/new");await z.wait({time:2});const $=await Z(z,G,{input:{uuid:"",title:K,rawBody:M,tagNames:Y,slide:!1,organizationId:null}}),Q=$?.saveCreatingArticle?.draftItem?.uuid;if(!Q)throw new X(`Qiita saveCreatingArticle returned no uuid: ${JSON.stringify($)}`);if(J.draft)return[{status:"draft",uuid:Q,url:`https://qiita.com/drafts/${Q}/edit`}];let O;if(J.secret)O=(await Z(z,P,{input:{uuid:Q,title:K,rawBody:M,tagNames:Y,slide:!1}}))?.publishSecretArticle?.article;else O=(await Z(z,I,{input:{uuid:Q,title:K,rawBody:M,tagNames:Y,slide:!1,tweetShare:!!J["tweet-share"],adventCalendarItems:[]}}))?.publishPublicArticle?.article;if(!O||!O.linkUrl)throw new X(`Qiita publish returned no article: ${JSON.stringify(O)}`);return[{status:J.secret?"published-secret":"published",uuid:O.uuid||Q,url:O.linkUrl}]}});V({site:"qiita",name:"delete",access:"write",description:"Delete a Qiita article or draft by its URL (article linkUrl, or https://qiita.com/drafts/<uuid>).",domain:"qiita.com",strategy:v.COOKIE,browser:!0,args:[{name:"url",type:"string",required:!1,help:"Article URL (e.g. https://qiita.com/<urlName>/items/<uuid>) or draft URL (https://qiita.com/drafts/<uuid>)"}],columns:["status","url"],func:async(z,J)=>{if(!z)throw new X("Browser session required for qiita delete");const K=String(J.url??"").trim();if(!/^https:\/\/qiita\.com\/.+/.test(K))throw new W("--url is required (a https://qiita.com/... article or draft URL)");await z.goto("https://qiita.com/");await z.wait({time:1});const M=await H(z,K);if(!M.ok)throw new X(`Qiita delete failed (HTTP ${M.status}) for ${K}`);return[{status:"deleted",url:K}]}});
7
+ }`;v({site:"qiita",name:"publish",access:"write",description:"Publish a Qiita article (markdown). Tags required. Public by default; --draft or --secret to change.",domain:"qiita.com",strategy:F.COOKIE,browser:!0,args:[{name:"title",type:"string",required:!1,help:"Article title (required)"},{name:"body",type:"string",required:!1,help:"Article body in Markdown (or use --body-file)"},{name:"body-file",type:"string",required:!1,help:"Path to a Markdown file for the body"},{name:"tags",type:"string",required:!1,help:"Comma-separated tags (REQUIRED, at least 1)"},{name:"draft",type:"boolean",required:!1,default:!1,help:"Save as draft only (do not publish)"},{name:"secret",type:"boolean",required:!1,default:!1,help:"Publish as limited-public (secret) instead of public"},{name:"tweet-share",type:"boolean",required:!1,default:!1,help:"Also share to X/Twitter on publish (public only)"}],columns:["status","uuid","url"],func:async(z,J)=>{if(!z)throw new Y("Browser session required for qiita publish");const M=String(J.title??"").trim(),K=resolveBody(J),O=resolveTagNames(J.tags);if(!M)throw new X("--title is required");if(K===void 0)throw new X("--body or --body-file is required");if(O.length===0)throw new X("--tags is required (Qiita articles need at least one tag)");assertNoLocalImages(K);await z.goto("https://qiita.com/drafts/new");await z.wait({time:2});const $=await Z(z,G,{input:{uuid:"",title:M,rawBody:K,tagNames:O,slide:!1,organizationId:null}}),W=$?.saveCreatingArticle?.draftItem?.uuid;if(!W)throw new Y(`Qiita saveCreatingArticle returned no uuid: ${JSON.stringify($)}`);if(J.draft)return[{status:"draft",uuid:W,url:`https://qiita.com/drafts/${W}/edit`}];let Q;if(J.secret)Q=(await Z(z,T,{input:{uuid:W,title:M,rawBody:K,tagNames:O,slide:!1}}))?.publishSecretArticle?.article;else Q=(await Z(z,P,{input:{uuid:W,title:M,rawBody:K,tagNames:O,slide:!1,tweetShare:!!J["tweet-share"],adventCalendarItems:[]}}))?.publishPublicArticle?.article;if(!Q||!Q.linkUrl)throw new Y(`Qiita publish returned no article: ${JSON.stringify(Q)}`);return[{status:J.secret?"published-secret":"published",uuid:Q.uuid||W,url:Q.linkUrl}]}});v({site:"qiita",name:"delete",access:"write",description:"Delete a Qiita article or draft by its URL (article linkUrl, or https://qiita.com/drafts/<uuid>).",domain:"qiita.com",strategy:F.COOKIE,browser:!0,args:[{name:"url",type:"string",required:!1,help:"Article URL (e.g. https://qiita.com/<urlName>/items/<uuid>) or draft URL (https://qiita.com/drafts/<uuid>)"}],columns:["status","url"],func:async(z,J)=>{if(!z)throw new Y("Browser session required for qiita delete");const M=String(J.url??"").trim();if(!/^https:\/\/qiita\.com\/.+/.test(M))throw new X("--url is required (a https://qiita.com/... article or draft URL)");await z.goto("https://qiita.com/");await z.wait({time:1});const K=await R(z,M);if(!K.ok)throw new Y(`Qiita delete failed (HTTP ${K.status}) for ${M}`);return[{status:"deleted",url:M}]}});
@@ -1,3 +1,3 @@
1
- import{CliError as O,CommandExecutionError as v}from"@jackwener/opencli/errors";import{cli as y,Strategy as C}from"@jackwener/opencli/registry";import{publishArticle as p}from"../_shared/article/publish.js";import{readFile as f,stat as u}from"node:fs/promises";function I(X){if(!X.execute)throw new O("INVALID_INPUT","本命令需要 --execute 才会真正发布,请确认后加上 --execute。注意:本报错仅表示缺少 --execute,并未校验其余参数,不能当 dry-run 预览用")}async function h(X){const A=typeof X.text==="string"?X.text:void 0,W=typeof X.file==="string"?X.file:void 0;if(A&&W)throw new O("INVALID_INPUT","请只传 <text> 或 --file 之一,不能同时使用");let G=A??"";if(W){let Z;try{Z=await u(W)}catch{throw new O("INVALID_INPUT",`文件不存在:${W}`)}if(!Z.isFile())throw new O("INVALID_INPUT",`路径必须是可读文本文件:${W}`);let M;try{M=await f(W)}catch{throw new O("INVALID_INPUT",`无法读取文件:${W}`)}try{G=new TextDecoder("utf-8",{fatal:!0}).decode(M)}catch{throw new O("INVALID_INPUT",`文件编码不是 UTF-8:${W}`)}}if(!G.trim())throw new O("INVALID_INPUT","正文不能为空");return G}function w(X,A,W,G,Z={}){return[{status:"success",outcome:G,message:X,target_type:A,target:W,...Z}]}export const segmentfaultProfile={home:"https://segmentfault.com/write",outputFormat:"markdown",image:{skip:["image-static.segmentfault.com","avatar-static.segmentfault.com"],uploadFn:async(X,A)=>{const G=await(await fetch("https://segmentfault.com/write",{credentials:"include"})).text();let Z=null;const M=G.match(/serverData"\s*:\s*\{\s*"Token"\s*:\s*"([^"]+)"/);if(M)Z=M[1];else{const _="window.g_initialProps = ",U=G.indexOf(_);if(U!==-1){const T=G.indexOf(`;
2
- </script>`,U);if(T!==-1)try{const V=JSON.parse(G.substring(U+_.length,T));Z=V&&V.global&&V.global.sessionInfo&&V.global.sessionInfo.key}catch(V){}}}if(!Z)throw Error("获取思否 session token 失败");const b=await fetch(X,{credentials:"omit"});if(!b.ok)throw Error("下载图片失败:"+b.status);const R=await b.blob(),N=new FormData;N.append("image",R);const L=await(await fetch("https://segmentfault.com/gateway/image",{method:"POST",credentials:"include",headers:{token:Z},body:N})).text();if(L==="Unauthorized"||L.includes("禁言")||L.includes("锁定"))throw Error(L==="Unauthorized"?"未授权,请检查登录态":L);let $;try{$=JSON.parse(L)}catch(_){throw Error("图片上传失败:"+L)}let B=null;if($&&$.result)B=$.result;else if(Array.isArray($)){if($[0]===1)throw Error($[1]||"图片上传失败");B=$[1]||($[2]?"https://image-static.segmentfault.com/"+$[2]:null)}if(!B)throw Error("图片上传失败,服务端未返回 URL");return{url:B}}},publish:async(X,A)=>{const W=X.params||{},Z=await(await fetch("https://segmentfault.com/write",{credentials:"include"})).text();let M=null;const b=Z.match(/serverData"\s*:\s*\{\s*"Token"\s*:\s*"([^"]+)"/);if(b)M=b[1];else{const Q="window.g_initialProps = ",z=Z.indexOf(Q);if(z!==-1){const J=Z.indexOf(`;
3
- </script>`,z);if(J!==-1)try{const K=JSON.parse(Z.substring(z+Q.length,J));M=K&&K.global&&K.global.sessionInfo&&K.global.sessionInfo.key}catch(K){}}}if(!M)return{ok:!1,stage:"token",status:0,message:"获取思否 session token 失败,请确认已登录"};async function R(Q){let z=null;try{const P=(await fetch("/",{method:"HEAD"})).headers.get("date");if(P){const q=new Date(P).getTime();if(!isNaN(q))z=Math.floor(q/1000)}}catch(Y){}const J=Object.keys(window).find((Y)=>Y.startsWith("webpackChunk"));if(!J)return{__sfErr:"页面未就绪:未找到 webpack 运行时,无法复用思否请求签名"};let K;try{window[J].push([[Symbol("pp_sf_"+Math.random())],{},(Y)=>{K=Y}])}catch(Y){return{__sfErr:"注入 webpack 探针失败:"+String(Y)}}if(!K||!K.m)return{__sfErr:"无法取得 webpack require(思否前端结构可能已变)"};let H=null;for(const Y of Object.keys(K.m)){let P="";try{P=String(K.m[Y])}catch(q){continue}if(/function getUrl/.test(P)&&P.indexOf("/gateway")!==-1){H=Y;break}}if(!H)return{__sfErr:"未定位到思否请求签名模块(getUrl/gateway),站点前端结构可能已变,需更新 adapter"};let F;try{F=K(H).default}catch(Y){return{__sfErr:"加载思否请求模块失败:"+String(Y)}}if(typeof F!=="function")return{__sfErr:"思否请求模块导出异常(default 非构造器)"};let D=null;const S=XMLHttpRequest.prototype.open;XMLHttpRequest.prototype.open=function(Y,P){try{if(String(P).indexOf(Q)!==-1)D=this}catch(q){}return S.apply(this,arguments)};try{const Y=new F({Token:M,userAgent:navigator.userAgent});if(z)Y.setServerTime(z);await Y.send(Q,{method:"GET"}).catch(function(){})}catch(Y){XMLHttpRequest.prototype.open=S;return{__sfErr:"签名请求执行失败:"+String(Y)}}XMLHttpRequest.prototype.open=S;if(!D)return{__sfErr:"签名请求未发出(Request.send 未触发 XHR)"};return{status:D.status,raw:String(D.responseText||"")}}if(X.draftOnly){const Q={title:X.title,tags:[],text:X.content,object_id:"",type:"article"},z=await fetch("https://segmentfault.com/gateway/draft",{method:"POST",credentials:"include",headers:{"Content-Type":"application/json",token:M,accept:"*/*"},body:JSON.stringify(Q)}),J=await z.text();if(J==="Unauthorized"||J.includes("禁言")||J.includes("锁定"))return{ok:!1,stage:"publish",status:z.status,message:J==="Unauthorized"?"未授权":J};let K;try{K=JSON.parse(J)}catch(H){return{ok:!1,stage:"publish",status:z.status,message:"解析响应失败:"+J.slice(0,200)}}if(Array.isArray(K)){if(K[0]===1)return{ok:!1,stage:"publish",status:z.status,message:K[1]||"发布失败"};const H=K[1];if(H&&H.id)return{ok:!0,draft:!0,id:String(H.id),url:"https://segmentfault.com/write?draftId="+H.id}}if(!K||!K.id){const H=K&&(K.message||K.msg||K.error||K.errMsg)||J.slice(0,200);return{ok:!1,stage:"publish",status:z.status,message:H}}return{ok:!0,draft:!0,id:String(K.id),url:"https://segmentfault.com/write?draftId="+K.id}}const N={"Content-Type":"application/json",token:M,accept:"*/*"},j=Array.isArray(W.tags)?W.tags.map((Q)=>String(Q).trim()).filter(Boolean):[];if(j.length===0)return{ok:!1,stage:"tags",message:"思否发文必须至少指定 1 个标签(--tags),合法标签可用 `segmentfault tags <名称>` 校验"};const L=[];for(const Q of j){const z=await R("/tag/"+encodeURIComponent(Q));if(z&&z.__sfErr)return{ok:!1,stage:"tags",message:"标签「"+Q+"」校验失败:"+z.__sfErr};let J;try{J=JSON.parse(z.raw)}catch(H){return{ok:!1,stage:"tags",status:z.status,message:"解析标签「"+Q+"」响应失败(HTTP "+z.status+"):"+String(z.raw).slice(0,200)}}if(z.status!==200||typeof J==="string")return{ok:!1,stage:"tags",status:z.status,message:"思否标签接口请求被拒(HTTP "+z.status+"):"+(typeof J==="string"?J:String(z.raw).slice(0,120))};const K=J&&J.tag;if(!K||K.id==null)return{ok:!1,stage:"tags",message:"思否上找不到标签「"+Q+"」,请用 `segmentfault tags <名称>` 确认存在再发布"};L.push(K.id)}const $=Array.isArray(W.channels)?W.channels.map((Q)=>String(Q).trim()).filter(Boolean):[],B=[];if($.length>0){let Q=[];const z=await R("/channels");if(z&&z.__sfErr)return{ok:!1,stage:"channels",message:"获取思否频道列表失败:"+z.__sfErr};let J;try{J=JSON.parse(z.raw)}catch(K){return{ok:!1,stage:"channels",status:z.status,message:"解析频道列表失败(HTTP "+z.status+"):"+String(z.raw).slice(0,200)}}if(z.status!==200||typeof J==="string")return{ok:!1,stage:"channels",status:z.status,message:"思否频道接口请求被拒(HTTP "+z.status+"):"+(typeof J==="string"?J:String(z.raw).slice(0,120))};if(Array.isArray(J))Q=J;else if(J&&Array.isArray(J.data))Q=J.data;else if(J&&Array.isArray(J.rows))Q=J.rows;else if(J&&Array.isArray(J.channels))Q=J.channels;else Q=[];for(const K of $){const H=Q.find((F)=>F&&(String(F.name||F.title||F.text)===K||String(F.slug)===K));if(!H||!H.slug){const F=Q.map((D)=>D&&(D.name||D.title||D.text)||"").filter(Boolean).join(" / ");return{ok:!1,stage:"channels",message:"思否上找不到频道「"+K+"」,可选:"+(F||"(频道列表为空)")}}B.push(H.slug)}}const _={title:X.title,text:X.content,tags:L};if(B.length>0)_.channel=B;if(W.cover)_.cover=String(W.cover);const U=await fetch("https://segmentfault.com/gateway/article",{method:"POST",credentials:"include",headers:N,body:JSON.stringify(_)}),T=await U.text();if(T==="Unauthorized"||T.includes("禁言")||T.includes("锁定"))return{ok:!1,stage:"publish",status:U.status,message:T==="Unauthorized"?"未授权,请检查登录态":T};let V;try{V=JSON.parse(T)}catch(Q){return{ok:!1,stage:"publish",status:U.status,message:"解析发布响应失败:"+T.slice(0,200)}}if(V&&(V.isError||V.error||V.errno||V.scene_id)){let Q=V.message||V.msg||V.error||"";if(V.scene_id)Q=(Q?Q+";":"")+"思否要求人机验证(geetest),无法在自动化环境完成";return{ok:!1,stage:"publish",status:U.status,message:Q||"发布失败:"+T.slice(0,200)}}const E=V&&V.data&&V.data.id;if(E==null)return{ok:!1,stage:"publish",status:U.status,message:V&&(V.message||V.msg)||"发布响应缺少文章 id:"+T.slice(0,200)};return{ok:!0,draft:!1,id:String(E),url:"https://segmentfault.com/a/"+E}}};y({site:"segmentfault",name:"article",access:"write",description:"发布文章到思否(SegmentFault)。默认正式发布,加 --draft 仅存草稿。正文默认 Markdown;外链图片自动转存到思否图床。正式发布必须指定标签(--tags),合法标签用 `segmentfault tags <名称>` 校验、频道用 `segmentfault channels` 列举。",domain:"segmentfault.com",strategy:C.COOKIE,browser:!0,args:[{name:"title",positional:!0,required:!0,help:"文章标题"},{name:"text",positional:!0,help:"文章正文(默认 Markdown;传 --html 则视为 HTML)"},{name:"file",help:"正文文件路径(UTF-8,默认 Markdown)"},{name:"html",type:"boolean",help:"将正文视为原始 HTML 而非 Markdown"},{name:"tags",help:"标签名,逗号分隔(正式发布必填,至少 1 个)。合法标签用 `segmentfault tags <名称>` 校验;草稿可省略。"},{name:"cover",help:"封面图思否图床 url(选填)。需先把图片上传到思否图床得到 url。"},{name:"channels",help:"频道名,逗号分隔(选填)。合法值用 `segmentfault channels` 列举。"},{name:"draft",type:"boolean",help:"仅保存草稿,不正式发布"},{name:"execute",type:"boolean",help:"实际执行发布。不加此参数时命令拒绝写操作。"}],columns:["status","outcome","message","target_type","target","created_target","created_url"],func:async(X,A)=>{if(!X)throw new v("思否发布需要浏览器会话");I(A);const W=String(A.title??"").trim();if(!W)throw new O("INVALID_INPUT","文章标题不能为空");const G=await h(A),Z=Boolean(A.draft),M=(_)=>typeof _==="string"?_.split(",").map((U)=>U.trim()).filter(Boolean):[],b=M(A.tags),R=M(A.channels),N=typeof A.cover==="string"?A.cover.trim():"";if(!Z&&b.length===0)throw new O("INVALID_INPUT","思否正式发布必须指定 --tags(至少 1 个标签);合法标签用 `segmentfault tags <名称>` 校验,或加 --draft 只存草稿");const j=await p(X,{title:W,body:G,format:A.html?"html":"markdown",draftOnly:Z,profile:segmentfaultProfile,publishParams:{tags:b,channels:R,cover:N}}),L=j.images.uploaded.length|0,$=j.images.failed.length|0;let B=j.draft?"已保存到思否草稿箱(需在网页上手动发布)":"已正式发布到思否";if(L||$)B+=`;图片:${L} 张转存成功${$?`,${$} 张失败`:""}`;return w(B,"article","",j.draft?"draft":"created",{created_target:(j.draft?"draft:":"article:")+j.id,created_url:j.url})}});
1
+ import{CliError as N,CommandExecutionError as P}from"@jackwener/opencli/errors";import{cli as v,Strategy as p}from"@jackwener/opencli/registry";import{publishArticle as f}from"../_shared/article/publish.js";import{readFile as I,stat as w}from"node:fs/promises";function u(W){if(!W.execute)throw new N("INVALID_INPUT","本命令需要 --execute 才会真正发布,请确认后加上 --execute。注意:本报错仅表示缺少 --execute,并未校验其余参数,不能当 dry-run 预览用")}async function h(W){const H=typeof W.text==="string"?W.text:void 0,V=typeof W.file==="string"?W.file:void 0;if(H&&V)throw new N("INVALID_INPUT","请只传 <text> 或 --file 之一,不能同时使用");let B=H??"";if(V){let A;try{A=await w(V)}catch{throw new N("INVALID_INPUT",`文件不存在:${V}`)}if(!A.isFile())throw new N("INVALID_INPUT",`路径必须是可读文本文件:${V}`);let M;try{M=await I(V)}catch{throw new N("INVALID_INPUT",`无法读取文件:${V}`)}try{B=new TextDecoder("utf-8",{fatal:!0}).decode(M)}catch{throw new N("INVALID_INPUT",`文件编码不是 UTF-8:${V}`)}}if(!B.trim())throw new N("INVALID_INPUT","正文不能为空");return B}function x(W,H,V,B,A={}){return[{status:"success",outcome:B,message:W,target_type:H,target:V,...A}]}export const segmentfaultProfile={home:"https://segmentfault.com/write",outputFormat:"markdown",image:{skip:["image-static.segmentfault.com","avatar-static.segmentfault.com"],uploadFn:async(W,H)=>{const B=await(await fetch("https://segmentfault.com/write",{credentials:"include"})).text();let A=null;const M=B.match(/serverData"\s*:\s*\{\s*"Token"\s*:\s*"([^"]+)"/);if(M)A=M[1];else{const U="window.g_initialProps = ",R=B.indexOf(U);if(R!==-1){const q=B.indexOf(`;
2
+ </script>`,R);if(q!==-1)try{const L=JSON.parse(B.substring(R+U.length,q));A=L&&L.global&&L.global.sessionInfo&&L.global.sessionInfo.key}catch(L){}}}if(!A)throw Error("获取思否 session token 失败");const O=await fetch(W,{credentials:"omit"});if(!O.ok)throw Error("下载图片失败:"+O.status);const b=await O.blob(),E=new FormData;E.append("image",b);const F=await(await fetch("https://segmentfault.com/gateway/image",{method:"POST",credentials:"include",headers:{token:A},body:E})).text();if(F==="Unauthorized"||F.includes("禁言")||F.includes("锁定"))throw Error(F==="Unauthorized"?"未授权,请检查登录态":F);let X;try{X=JSON.parse(F)}catch(U){throw Error("图片上传失败:"+F)}let T=null;if(X&&X.url)T=X.url;else if(X&&X.result)T=X.result;else if(Array.isArray(X)){if(X[0]===1)throw Error(X[1]||"图片上传失败");T=X[1]||(X[2]?"https://image-static.segmentfault.com/"+X[2]:null)}if(!T)throw Error("图片上传失败,服务端未返回 URL");return{url:T}}},publish:async(W,H)=>{const V=W.params||{},A=await(await fetch("https://segmentfault.com/write",{credentials:"include"})).text();let M=null;const O=A.match(/serverData"\s*:\s*\{\s*"Token"\s*:\s*"([^"]+)"/);if(O)M=O[1];else{const Q="window.g_initialProps = ",z=A.indexOf(Q);if(z!==-1){const J=A.indexOf(`;
3
+ </script>`,z);if(J!==-1)try{const K=JSON.parse(A.substring(z+Q.length,J));M=K&&K.global&&K.global.sessionInfo&&K.global.sessionInfo.key}catch(K){}}}if(!M)return{ok:!1,stage:"token",status:0,message:"获取思否 session token 失败:请确认已登录;账号首次发文会被新手确认页(/howtowrite)拦住,需先在网页端进一次写作页点「我已知晓,继续撰写」"};async function b(Q){let z=null;try{const D=(await fetch("/",{method:"HEAD"})).headers.get("date");if(D){const S=new Date(D).getTime();if(!isNaN(S))z=Math.floor(S/1000)}}catch($){}const J=Object.keys(window).find(($)=>$.startsWith("webpackChunk"));if(!J)return{__sfErr:"页面未就绪:未找到 webpack 运行时,无法复用思否请求签名"};let K;try{window[J].push([[Symbol("pp_sf_"+Math.random())],{},($)=>{K=$}])}catch($){return{__sfErr:"注入 webpack 探针失败:"+String($)}}if(!K||!K.m)return{__sfErr:"无法取得 webpack require(思否前端结构可能已变)"};let Y=null;for(const $ of Object.keys(K.m)){let D="";try{D=String(K.m[$])}catch(S){continue}if(/function getUrl/.test(D)&&D.indexOf("/gateway")!==-1){Y=$;break}}if(!Y)return{__sfErr:"未定位到思否请求签名模块(getUrl/gateway),站点前端结构可能已变,需更新 adapter"};let j;try{j=K(Y).default}catch($){return{__sfErr:"加载思否请求模块失败:"+String($)}}if(typeof j!=="function")return{__sfErr:"思否请求模块导出异常(default 非构造器)"};let Z=null;const C=XMLHttpRequest.prototype.open;XMLHttpRequest.prototype.open=function($,D){try{if(String(D).indexOf(Q)!==-1)Z=this}catch(S){}return C.apply(this,arguments)};try{const $=new j({Token:M,userAgent:navigator.userAgent});if(z)$.setServerTime(z);await $.send(Q,{method:"GET"}).catch(function(){})}catch($){XMLHttpRequest.prototype.open=C;return{__sfErr:"签名请求执行失败:"+String($)}}XMLHttpRequest.prototype.open=C;if(!Z)return{__sfErr:"签名请求未发出(Request.send 未触发 XHR)"};return{status:Z.status,raw:String(Z.responseText||"")}}if(W.draftOnly){const Q={title:W.title,tags:[],text:W.content,object_id:"",type:"article"},z=await fetch("https://segmentfault.com/gateway/draft",{method:"POST",credentials:"include",headers:{"Content-Type":"application/json",token:M,accept:"*/*"},body:JSON.stringify(Q)}),J=await z.text();if(J==="Unauthorized"||J.includes("禁言")||J.includes("锁定"))return{ok:!1,stage:"publish",status:z.status,message:J==="Unauthorized"?"未授权":J};let K;try{K=JSON.parse(J)}catch(Y){return{ok:!1,stage:"publish",status:z.status,message:"解析响应失败:"+J.slice(0,200)}}if(Array.isArray(K)){if(K[0]===1)return{ok:!1,stage:"publish",status:z.status,message:K[1]||"发布失败"};const Y=K[1];if(Y&&Y.id)return{ok:!0,draft:!0,id:String(Y.id),url:"https://segmentfault.com/write?draftId="+Y.id}}if(!K||!K.id){const Y=K&&(K.message||K.msg||K.error||K.errMsg)||J.slice(0,200);return{ok:!1,stage:"publish",status:z.status,message:Y}}return{ok:!0,draft:!0,id:String(K.id),url:"https://segmentfault.com/write?draftId="+K.id}}const E={"Content-Type":"application/json",token:M,accept:"*/*"},_=Array.isArray(V.tags)?V.tags.map((Q)=>String(Q).trim()).filter(Boolean):[];if(_.length===0)return{ok:!1,stage:"tags",message:"思否发文必须至少指定 1 个标签(--tags),合法标签可用 `segmentfault tags <名称>` 校验"};const F=[];for(const Q of _){const z=await b("/tag/"+encodeURIComponent(Q));if(z&&z.__sfErr)return{ok:!1,stage:"tags",message:"标签「"+Q+"」校验失败:"+z.__sfErr};let J;try{J=JSON.parse(z.raw)}catch(Y){return{ok:!1,stage:"tags",status:z.status,message:"解析标签「"+Q+"」响应失败(HTTP "+z.status+"):"+String(z.raw).slice(0,200)}}if(z.status!==200||typeof J==="string")return{ok:!1,stage:"tags",status:z.status,message:"思否标签接口请求被拒(HTTP "+z.status+"):"+(typeof J==="string"?J:String(z.raw).slice(0,120))};const K=J&&J.tag;if(!K||K.id==null)return{ok:!1,stage:"tags",message:"思否上找不到标签「"+Q+"」,请用 `segmentfault tags <名称>` 确认存在再发布"};F.push(K.id)}const X=Array.isArray(V.channels)?V.channels.map((Q)=>String(Q).trim()).filter(Boolean):[],T=[];if(X.length>0){let Q=[];const z=await b("/channels");if(z&&z.__sfErr)return{ok:!1,stage:"channels",message:"获取思否频道列表失败:"+z.__sfErr};let J;try{J=JSON.parse(z.raw)}catch(K){return{ok:!1,stage:"channels",status:z.status,message:"解析频道列表失败(HTTP "+z.status+"):"+String(z.raw).slice(0,200)}}if(z.status!==200||typeof J==="string")return{ok:!1,stage:"channels",status:z.status,message:"思否频道接口请求被拒(HTTP "+z.status+"):"+(typeof J==="string"?J:String(z.raw).slice(0,120))};if(Array.isArray(J))Q=J;else if(J&&Array.isArray(J.data))Q=J.data;else if(J&&Array.isArray(J.rows))Q=J.rows;else if(J&&Array.isArray(J.channels))Q=J.channels;else Q=[];for(const K of X){const Y=Q.find((j)=>j&&(String(j.name||j.title||j.text)===K||String(j.slug)===K));if(!Y||!Y.slug){const j=Q.map((Z)=>Z&&(Z.name||Z.title||Z.text)||"").filter(Boolean).join(" / ");return{ok:!1,stage:"channels",message:"思否上找不到频道「"+K+"」,可选:"+(j||"(频道列表为空)")}}T.push(Y.slug)}}let U="";if(V.cover){const Q=String(V.cover);if(Q.indexOf("/img/")===0)U=Q;else{let z;try{const Z=await fetch(Q,{credentials:"omit"});if(!Z.ok)return{ok:!1,stage:"cover",status:Z.status,message:"封面图下载失败(HTTP "+Z.status+"):"+Q.slice(0,120)};z=await Z.blob()}catch(Z){return{ok:!1,stage:"cover",message:"封面图下载失败("+String(Z&&Z.message||Z)+")。思否封面只收自家图床 /img/ 路径;建议 --cover 直接传本机图片路径,由命令自动转存"}}const J=new FormData;J.append("image",z);const K=await fetch("https://segmentfault.com/gateway/image",{method:"POST",credentials:"include",headers:{token:M},body:J}),Y=await K.text();let j;try{j=JSON.parse(Y)}catch(Z){return{ok:!1,stage:"cover",status:K.status,message:"封面图上传思否图床失败:"+Y.slice(0,120)}}U=j&&j.url||"";if(!U)return{ok:!1,stage:"cover",status:K.status,message:"封面图上传后未拿到 /img/ 路径:"+Y.slice(0,120)}}}const R={title:W.title,text:W.content,tags:F,blog_id:0,type:1};if(T.length>0)R.channel=T;if(U)R.cover=U;const q=await fetch("https://segmentfault.com/gateway/article",{method:"POST",credentials:"include",headers:E,body:JSON.stringify(R)}),L=await q.text();if(L==="Unauthorized"||L.includes("禁言")||L.includes("锁定"))return{ok:!1,stage:"publish",status:q.status,message:L==="Unauthorized"?"未授权,请检查登录态":L};let G;try{G=JSON.parse(L)}catch(Q){return{ok:!1,stage:"publish",status:q.status,message:"解析发布响应失败:"+L.slice(0,200)}}if(G&&(G.isError||G.error||G.errno||G.scene_id)){let Q=G.message||G.msg||G.error||"";if(G.scene_id)Q=(Q?Q+";":"")+"思否要求人机验证(geetest),无法在自动化环境完成";return{ok:!1,stage:"publish",status:q.status,message:Q||"发布失败:"+L.slice(0,200)}}const y=G&&G.data&&G.data.id;if(y==null)return{ok:!1,stage:"publish",status:q.status,message:G&&(G.message||G.msg)||"发布响应缺少文章 id:"+L.slice(0,200)};return{ok:!0,draft:!1,id:String(y),url:"https://segmentfault.com/a/"+y}}};v({site:"segmentfault",name:"article",access:"write",description:"发布文章到思否(SegmentFault)。默认正式发布,加 --draft 仅存草稿。正文默认 Markdown;外链图片自动转存到思否图床。正式发布必须指定标签(--tags),合法标签用 `segmentfault tags <名称>` 校验、频道用 `segmentfault channels` 列举。",domain:"segmentfault.com",strategy:p.COOKIE,browser:!0,args:[{name:"title",positional:!0,required:!0,help:"文章标题"},{name:"text",positional:!0,help:"文章正文(默认 Markdown;传 --html 则视为 HTML)"},{name:"file",help:"正文文件路径(UTF-8,默认 Markdown)"},{name:"html",type:"boolean",help:"将正文视为原始 HTML 而非 Markdown"},{name:"tags",help:"标签名,逗号分隔(正式发布必填,至少 1 个)。合法标签用 `segmentfault tags <名称>` 校验;草稿可省略。"},{name:"cover",help:"封面图(选填):本机图片路径或图片 URL,自动转存到思否图床。"},{name:"channels",help:"频道名,逗号分隔(选填)。合法值用 `segmentfault channels` 列举。"},{name:"draft",type:"boolean",help:"仅保存草稿,不正式发布"},{name:"execute",type:"boolean",help:"实际执行发布。不加此参数时命令拒绝写操作。"}],columns:["status","outcome","message","target_type","target","created_target","created_url"],func:async(W,H)=>{if(!W)throw new P("思否发布需要浏览器会话");u(H);const V=String(H.title??"").trim();if(!V)throw new N("INVALID_INPUT","文章标题不能为空");const B=await h(H),A=Boolean(H.draft),M=(U)=>typeof U==="string"?U.split(",").map((R)=>R.trim()).filter(Boolean):[],O=M(H.tags),b=M(H.channels),E=typeof H.cover==="string"?H.cover.trim():"";if(!A&&O.length===0)throw new N("INVALID_INPUT","思否正式发布必须指定 --tags(至少 1 个标签);合法标签用 `segmentfault tags <名称>` 校验,或加 --draft 只存草稿");const _=await f(W,{title:V,body:B,format:H.html?"html":"markdown",draftOnly:A,profile:segmentfaultProfile,publishParams:{tags:O,channels:b,cover:E}}),F=_.images.uploaded.length|0,X=_.images.failed.length|0;let T=_.draft?"已保存到思否草稿箱(需在网页上手动发布)":"已正式发布到思否";if(F||X)T+=`;图片:${F} 张转存成功${X?`,${X} 张失败`:""}`;return x(T,"article","",_.draft?"draft":"created",{created_target:(_.draft?"draft:":"article:")+_.id,created_url:_.url})}});
@@ -1 +1 @@
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 才会真正写入。注意:本报错仅表示缺少 --execute,并未校验其余参数,不能当 dry-run 预览用")}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
+ 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 H of L.data.data)if(H.accounts)G.push(...H.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(),M=new FormData;M.append("file",Y,"image.jpg");M.append("accountId",X);const W=await(await fetch("https://mp.sohu.com/commons/front/outerUpload/image/file?accountId="+X,{method:"POST",credentials:"include",body:M})).json();if(!W.url)throw Error("搜狐图片上传失败:"+(W.msg||JSON.stringify(W).slice(0,100)));return{url:W.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 F of G.data.data)if(F.accounts)X.push(...F.accounts);if(!X.length)return{ok:!1,stage:"auth",status:0,message:"搜狐号子账号列表为空"};const V=Number(X[0].id),Y="0123456789abcdef";let M="";for(let F=0;F<32;F++)M+=Y[Math.floor(Math.random()*Y.length)];const B="100-"+Date.now()+"-"+M,W=Number(z.channelId);if(!Number.isFinite(W)||W<=0)return{ok:!1,stage:"params",status:0,message:"缺少合法 channelId(频道 id),请先用 `sohu channels` 取值"};const H=z.categoryId==null||z.categoryId===""?-1:Number(z.categoryId),_={title:Q.title,brief:typeof z.brief==="string"?z.brief:"",content:Q.content,channelId:W,categoryId:H,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 F=Object.assign({},_,{auto:!0}),q=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":M,"sp-cm":B},body:JSON.stringify(F)}),x=await q.text();let D=null;try{D=JSON.parse(x)}catch(i){}if(!q.ok||!D||!D.success)return{ok:!1,stage:"draft",status:q.status,message:D&&D.msg||x.slice(0,300)};const A=String(D.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 U=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":M,"sp-cm":B},body:JSON.stringify(_)}),S=await U.text();let Z=null;try{Z=JSON.parse(S)}catch(F){}if(!U.ok||!Z||Z.code!==2000000){let F=Z&&(Z.msg||Z.message)||"";if(!F){const q=[];if(H!==-1)q.push("--category");if(Array.isArray(z.topicIds)&&z.topicIds.length>0)q.push("--topics");if(_.cover)q.push("--cover");F="搜狐发布接口返回非 JSON 错误页(HTTP "+U.status+")。"+(q.length?"实测该 500 多由可选参数触发——去掉 "+q.join(" / ")+"、只保留必填 --channel 重试即可。":"稍后重试;若持续失败,用 `sohu stats` 确认是否已产生重复内容。")}return{ok:!1,stage:"publish",status:U.status,message:F}}if(Z.data!=null&&Object.prototype.toString.call(Z.data)==="[object Object]")return{ok:!1,stage:"origin_check",status:U.status,message:"声明原创触发了原创校验(需在搜狐后台完成原创确认):"+JSON.stringify(Z.data).slice(0,200)};const J=String(Z.data),T="https://mp.sohu.com/mpfe/v4/contentManagement/news/addarticle?spm=smmp.articlelist.0.0&contentStatus=1&id="+J;return{ok:!0,id:J,url:T,draft:!1}}},sohuAuthProfile={home:sohuProfile.home,checkAuth:sohuProfile.checkAuth};function y(Q){if(!Q.execute)throw new $("INVALID_INPUT","此命令需要 --execute 才会真正写入。注意:本报错仅表示缺少 --execute,并未校验其余参数,不能当 dry-run 预览用")}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 O(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 O(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 O(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 O(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 O(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 O(Q,"https://mp.sohu.com/mpbp/bp/news/v4/label/topic/search?accountId="+K+"&keyword="+encodeURIComponent(X),"话题搜索"),M=(Array.isArray(V)?V:V.data||[]).find((B)=>String(B.name)===String(X));if(!M)throw new $("INVALID_INPUT","未找到话题「"+X+"」,请用 `sohu topics --keyword "+X+"` 搜索确认确切名称。");G.push(M.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 M=-1;const B=typeof K.category==="string"?K.category.trim():"";if(B)M=await h(Q,V,Y,B);let W=0;const H=typeof K.column==="string"?K.column.trim():"";if(H)W=await w(Q,V,H);let _=[];const U=typeof K.topics==="string"?K.topics.trim():"";if(U)_=await d(Q,V,U);const S={channelId:Y,categoryId:M,userColumnId:W,topicIds:_,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:S}),J=Z.images.uploaded.length|0,T=Z.images.failed.length|0;let F=Z.draft?"已保存搜狐号草稿":"已正式发布到搜狐号";if(J||T)F+=`;图片:${J} 张已转存${T?`,${T} 张失败`:""}`;return C(F,"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};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "publishport-opencli",
3
- "version": "1.8.5-pp.32",
3
+ "version": "1.8.5-pp.33",
4
4
  "publishConfig": {
5
5
  "access": "public",
6
6
  "provenance": false